memory
golang
OriginalFusic Day 23

Goでよりメモリ効率の高いプログラムを書く

社内でもGoではやってきている気がするので、今回はそれについて小ネタでも書きます。
最近はお仕事の新しい部分やツールでGoを書く機会が少しだけ増えているので、そこでした小手先のテクニックです。

必要なメモリ量が見積もれる場合は、キャパシティを設定する

ごくごく基本ではあるとは思うんですが、横着で map[string]string{} とか書いてしまう時がけっこうあるのでコードのリファクタリング時にけっこう大事です。
公式言語仕様 を見るとsliceとmapを make する時に初期キャパシティやサイズが指定できるので、必要サイズが見積もれる場合には極力指定しましょう。

サンプルにするとこんな感じです(mapmake するときに第2引数を指定しています):

package main

import "fmt"

type element struct {
  key, value string
}

func slice2map(ary []element) map[string]string {
  ret := make(map[string]string, len(ary))
  for _, v := range ary {
    ret[v.key] = v.value
  }
  return ret
}

func main() {
  ary := []element{
    element{key: "test", value: "test_value"},
    element{key: "test2", value: "test2_value"},
  }
  fmt.Println(slice2map(ary))
}

オブジェクトを極力お外に出さない

実際にメモリプロファイラとかで計測してからなのですが、長期稼働するプログラムだとけっこう大事な気がします。

スタック上にオブジェクトを置ける言語だと基本なんですが、スクリプト言語さわっていると忘れがちです。
公式のFAQを読むと、外に参照が出なければ、極力スタック上に確保する旨があります。

なので、関数が少し長くなっても、一時オブジェクトになってしまう場合は関数を分割しない方がよさそうです。
ただ、初期化関数は長くなりがちでかつ一度確保してしまえば後はGCされるだけなのでこの限りではないです。
ループ内でメモリ確保する場合は注意といった程度でしょうか。

そして、おそらく外に参照が出るといったケースで見落としそうなのが、ポインタ渡しされた構造体などの参照を書き換えた場合などです。
短期的に寿命を迎える場合は、極力関数内で終えられるように設計を変えるのを検討した方がいいかと思います。

素の構造体渡しを極力減らす

引数や戻り値型に * を追加していくだけの簡単な変更です。

ただ、参照透過性の観点からは、正直あんまり好きではないのですが、ポインタなら一度確保してしまえばコピーが発生しないようです。
ここらへんは、あまり言語仕様に詳しくない上に恐らく実装依存な部分も多そうなのでもっと調査したいところです。

注意するべきなのは、ポインタ渡しなので渡した構造体を変更されるとバグを起こしやすくなります。
なので、一度確保したら極力変更しないことです。自分の足を撃ってはいけません。
C++やRustだとここらへんはよく考えられていて、 const 修飾子などで変更ができない参照渡しにできたりします。

まとめ

Goでメモリチューニングをするといっても、結局GoなのでGCあったりなどの限界があって難しいので、更に省メモリ性能がほしい場合はRustやC++に書き換えるのを検討するべきかと思います。
(というか、実際に長期稼働するツールの省メモリ化などをしてみて、難しさをとても感じました。)
やはり、適材適所というのはいい言葉です。
Goのいいところは動的型付けなスクリプト言語からの手軽な移行なので、それ以上を求めるならやはり相応の言語でやるべきですね。