いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-362 (文字列とバイト列のクローン)(strings.Clone, bytes.Clone)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。

に載っていた内容になるのですが、ちょいちょい自分のコードでもやってしまうので、忘れないようにここにメモメモ。。。

Goの標準コンパイラでは、現状元文字列と部分文字列は同じメモリデータを共有するようになっています。(文字列は不変だから)

なので、処理上で大きな文字列の一部をどこかに保持しておくようなコードがある場合

store.Add(s[:5])

のように、そのままスライスして設定していると、上の例だと5バイト分だけ保持されるのではなくて、元の文字列がそのままメモリに残ることになります。

このような場合は、どの言語でも「ディープコピー」して設定しておくということをします。新たなメモリ割り当てを作って、そっちを保持するようにしておくことで、元データが不要となったらちゃんと解放されるようにする。

Goでは、スライスがとても簡単に利用できるので、ちょいちょいやってしまいます。

んで、Go 1.18 から strings.Clone() が追加されました。(bytes.Clone() は、Go 1.20 から)

これを使うと、新たなメモリ割り当てを用意してそこにコピーして返してくれます。

以下、ちょっとサンプル作って試してみました。

サンプル

内部で大きな文字列を確保している状態で、それらの部分文字列を別の場所に確保する処理を実施しています。

現状(2023-12-05 現在)のGoの標準コンパイラでは、元の文字列と部分文字列は同じメモリデータを共有するので

部分文字列をシャローコピーして別のストアに保持したままだと、メモリが開放されません。

strings.Clone() を利用することにより、ディープコピーが行われるので、メモリが開放されるようになります。

package main

import (
    "flag"
    "io"
    "log"
    "os/exec"
    "runtime"
    "strings"
    "unsafe"
)

const (
    NUM_ITEMS = 1000
    SHELL     = "/bin/bash"
)

var (
    store = make([]string, NUM_ITEMS)
)

func init() {
    log.SetFlags(0)
}

func mem(prefix string) {
    var (
        m = runtime.MemStats{}
    )

    runtime.ReadMemStats(&m)
    log.Printf("[%s]\t%8d\t%8d\n", prefix, m.HeapAlloc, m.HeapObjects)
}

func gen() []string {
    var (
        l = make([]string, NUM_ITEMS)
    )

    for i := 0; i < NUM_ITEMS; i++ {
        output, _ := exec.Command(SHELL, "-c", "openssl rand -base64 4096 | tr -d '\n'").Output()
        l[i] = unsafe.String(&output[0], len(output))
    }

    return l
}

func main() {
    log.Println("Title       \tHeapAlloc\tHeapObjects")
    mem("start     ")

    var (
        use = flag.Bool("use", false, "Use strings.Clone()")
    )
    flag.Parse()

    var (
        l = gen()
    )
    mem("gen       ")

    for i := 0; i < NUM_ITEMS; i++ {
        storeValue := l[i][:5]

        if *use {
            store[i] = strings.Clone(storeValue)
        } else {
            store[i] = storeValue
        }
    }
    mem("store     ")

    runtime.GC()

    for i, v := range store {
        if i%200 == 0 {
            runtime.GC()
            mem("checkpoint")
        }

        io.Discard.Write(unsafe.Slice(unsafe.StringData(v), len(v)))
    }
}

タスクファイルは以下。

# https://taskfile.dev

version: '3'

tasks:
  default:
    deps: [ build ]
    cmds:
      - task: run-not-use-clone
      - task: run-use-clone
  build:
    cmds:
      - go build -o app main.go
  run-not-use-clone:
    cmds:
      - ./app
  run-use-clone:
    cmds:
      - ./app -use

実行すると以下のような結果になります。

$ task
task: [build] go build -o app main.go
task: [run-not-use-clone] ./app
Title           HeapAlloc       HeapObjects
[start     ]      192792             144
[gen       ]    11482528            4576
[store     ]    11487008            4588
[checkpoint]     8471296            1363
[checkpoint]     8475728            1372
[checkpoint]     8475728            1372
[checkpoint]     8475728            1372
[checkpoint]     8475736            1373
task: [run-use-clone] ./app -use
Title           HeapAlloc       HeapObjects
[start     ]      192824             144
[gen       ]    11497632            4607
[store     ]    11507440            4952
[checkpoint]      296112             724
[checkpoint]      300536             732
[checkpoint]      300536             732
[checkpoint]      300544             733
[checkpoint]      300544             733

strings.Clone() を使っている方は、グンとメモリが解放されています。部分文字列を設定する際に strings.Clone() を使うことによりディープコピーが行われて、元文字列とメモリデータを共有しなくなっているので、GC後に元文字列のメモリが解放されているからですね。

参考情報

Goのおすすめ書籍

Go言語による並行処理

Go言語による並行処理

Amazon

上の書籍の日本語版が下です。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。