パフォーマンス維持のコツをコツコツとメモする
リフレクションは最後の手段
パフォーマンスに寄与しない部分でのみ使う。 どこがパフォーマンスに寄与するのかが不透明なうちは使用禁止のほうが良い。 一度使い出すとリフレクションは多用したくなる魔力がある。
メモリ使用量
- 値は8バイトアライメントに置かれるので基本は8バイト長分メモリを専有。
- ポインタ変数は64bitCPUで8バイト長
- インターフェース型変数は16バイト長〜 (値+型識別)
メモリ確保を含む型コンバートは 型キャスト、アサーションに比べると10倍以上遅い。
同じ値なのに「メモリ確保を含む型コンバート」を複数回行う場合は メモリ消費量は増えるが汎用の変数「interface{}」に 値を保存しておいて参照するほうが速度を維持できる。
ゼロメモリアロケーション
高頻度操作におけるメモリアロック1とゼロの間には大きな速度差がある。 可能であればゼロメモリアロケーション化を目指そう。 (事前確保やプール機構などの導入)
メモリアロケーション回避できない状況で プール機構とかはあまり頑張らないほうがいい。 (プールするためにメモリアロケーションが増えるようならNG)
stringをコンバートなしで[]byteに追記する方法があるが、 他の方法に比べて馬鹿っぱやいので この方法に落とし込めるかどうかは常に検討したほうが良い。
mapとsliceのインデックスアクセス
mapのインデックスアクセスはsliceの数十倍遅い。 100件以下の場合バイナリサーチでsliceから目的の値を探すほうが早い。 100要素超えくらいからmapのアクセス速度一定の恩恵が発揮される。
Goのコンストラクタ
new(Type)がもっとも基本で早い。その他のコンストラクト方法は「new+アルファ」で数ナノおそい。 (高頻度にメモリ確保する構造体はnewだけで済むデザインがベター。) ゼロ値で意図しない動作をしない設計を推奨。
例:デフォルトで有効であることを推奨するなら フィールド名は「〜Disable〜」という名称にするとか
Goのメモリアロケーション
- 小さめ(十数キロバイト以下)のメモリ確保と破棄が同じスタックで行われる場合はヒープを使わない。
- スタック上で処理されるのでメモリアロケーション操作数はゼロになる。
- スタックによるメモリ操作はヒープに比べて50倍程度早い。
スライスのメモリ使用量
スライス値は以下の3フィールドの構造体に相当する(3x8=24バイト長)。 これとは別に実際の配列データメモリ分のメモリを消費する。
- バッファへのポインタ
- データ長
- キャパシティ長
スライス値を別の変数にバインドすると上記24バイトのコピーを行う。 微々たる改善にしかならないが、スライスを引き渡すのに スライスへのポインタ型を使うとポインタ8バイトのコピーのみで渡せる。
文字列の結合&Writerへの出力
「bytes = append(bytes, str…)」が最速。
ベンチ: https://play.golang.org/p/10NVBfz2DW
package main
import (
"fmt"
"strings"
"testing"
"text/template"
)
var bob = &struct {
Name string
Age int
}{
Name: "Bob",
Age: 23,
}
type NullWriter struct{}
func (w *NullWriter) Write(b []byte) (int, error) { return len(b), nil }
func BenchmarkAppendStr(b *testing.B) {
w := &NullWriter{}
buff := []byte{}
for i := 0; i < b.N; i++ {
buff = buff[0:0]
buff = append(buff, "Hi, my name is "...)
buff = append(buff, bob.Name...)
buff = append(buff, " and I'm "...)
buff = append(buff, string(bob.Age)...)
buff = append(buff, " years old."...)
w.Write(buff)
}
}
func BenchmarkFmtFormat(b *testing.B) {
w := &NullWriter{}
for i := 0; i < b.N; i++ {
fmt.Fprintf(w, "Hi, my name is %s and I'm %d years old.", bob.Name, bob.Age)
}
}
func BenchmarkConcat1(b *testing.B) {
w := &NullWriter{}
for i := 0; i < b.N; i++ {
fmt.Fprint(w, "Hi, my name is "+bob.Name+" and I'm "+string(bob.Age)+" years old.")
}
}
func BenchmarkConcat2(b *testing.B) {
w := &NullWriter{}
for i := 0; i < b.N; i++ {
fmt.Fprint(w, strings.Join([]string{"Hi, my name is ", bob.Name, " and I'm ", string(bob.Age), " years old."}, ""))
}
}
func BenchmarkTemplate(b *testing.B) {
t := template.Must(template.New("").Parse(
`Hi, my name is {{.Name}} and I'm {{.Age}} years old.`))
w := &NullWriter{}
for i := 0; i < b.N; i++ {
t.Execute(w, bob)
}
}
計測例(Intel® Core™ i5-2467M CPU @ 1.60GHz)
$ go test -bench . -benchmem bytes_test.go
testing: warning: no tests to run
PASS
BenchmarkAppendStr-4 20000000 61.0 ns/op 0 B/op 0 allocs/op
BenchmarkFmtFormat-4 2000000 604 ns/op 24 B/op 2 allocs/op
BenchmarkConcat1-4 3000000 476 ns/op 64 B/op 2 allocs/op
BenchmarkConcat2-4 2000000 652 ns/op 116 B/op 4 allocs/op
BenchmarkTemplate-4 500000 2753 ns/op 88 B/op 5 allocs/op
ok command-line-arguments 8.445s
直書きとdefer
defer経由とdeferなしとで5倍くらい速度差がある。 でもdeferなしを使うのは囲んでるコードからreturnやbreak、panic等で すり抜けて関数が終了することがないのを確信できてる場合かつ 速度がシビアな場合だけに留めよう。 速度が重要でない場合や自分の管理外のコードが間にある場合は deferを使うべき。
ベンチ: https://play.golang.org/p/b5ZM1BW14M
package main
import (
"sync"
"testing"
)
var mu = sync.Mutex{}
func withoutDefer() {
mu.Lock()
mu.Unlock()
}
func withDefer() {
mu.Lock()
defer mu.Unlock()
}
func BenchmarkWithoutDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
withoutDefer()
}
}
func BenchmarkWithDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
withDefer()
}
}
計測例(Intel® Core™ i5-2467M CPU @ 1.60GHz)
$ go test -bench . -benchmem deffer_test.go
testing: warning: no tests to run
PASS
BenchmarkWithoutDefer-4 50000000 35.3 ns/op 0 B/op 0 allocs/op
BenchmarkWithDefer-4 10000000 184 ns/op 0 B/op 0 allocs/op
ok command-line-arguments 3.829s