GoのパフォーマンスTipsメモ


パフォーマンス維持のコツをコツコツとメモする

リフレクションは最後の手段

パフォーマンスに寄与しない部分でのみ使う。 どこがパフォーマンスに寄与するのかが不透明なうちは使用禁止のほうが良い。 一度使い出すとリフレクションは多用したくなる魔力がある。

メモリ使用量

  • 値は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

fmt.Fprintと[]byte変換

上記のベンチ、 「w.Write([]byte(str))」 じゃなく 「fmt.Fprint(w, str)」使ってるのなんで?という質問が知人からありましたが、 試してみればわかるけど実は後者のほうが早い。 []byteへの変換はメモリ確保とコピーが発生して意外と重いみたい。

あと、「buff = buff[0:0]」はスライスのキャパシティを維持したままデータ長をゼロにする方法です。

直書きと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