関連記事
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のおすすめ書籍
上の書籍の日本語版が下です。
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。