[Go]構造体フィールドの順番はパフォーマンスに影響するか?
背景
- Goの構造体は、後ろのフィールドにアクセスするのと最初のフィールドにアクセスするのではコストが違う
- なぜなら1番目のフィールドのアドレス=構造体そのもののアドレスであるため、フィールドアクセス時にメモリオフセットの計算がいらないから
という話をどこかで聞いたことがあったのです。
どこかにそういうブログ記事があったのかそれも忘れてしまったのですが(情報求む)...
これをきちんと「推測するな、計測しろ」というポリシーに則って検証してみようと思いました。
だいぶ時間経ってますが、この記事に似た内容が書かれてました。
検証環境
Goのバージョン
- go version go1.17.2 darwin/amd64
用意した構造体
int
型のフィールドを100個持つ構造体を用意しました。
type MyStruct struct {
Field1 int
Field2 int
Field3 int
// (以下略)
Field100 int
}
検証内容
- 1番目に定義された
Field1
を100回インクリメント - 100番目に定義された
Field100
を100回インクリメント
この2つの動作に有意なパフォーマンス差が生じるのかを検証します。
func IncField1() {
stc := MyStruct{}
for i := 0; i < 100; i++ {
stc.Field1++
}
}
func IncField100() {
stc := MyStruct{}
for i := 0; i < 100; i++ {
stc.Field100++
}
}
ベンチマークをとる
テストコード
以下のベンチマークテストを用意して実行しました。
func BenchmarkAccessField1(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
IncField1()
}
}
func BenchmarkAccessField100(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
IncField100()
}
}
結果
数回実行しただけで、平均取ったり分散取ったりとかきちんとしてない(いずれやりたいけど)ので考察には議論の余地ありだと思いますが……これ有意差ありますかね?ないと思っちゃうのですが……
$ go test -bench .
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkAccessField1-8 8629333 132.2 ns/op
BenchmarkAccessField100-8 8820518 132.2 ns/op
BenchmarkAccessField1-8 8634548 132.3 ns/op
BenchmarkAccessField100-8 8708960 133.4 ns/op
BenchmarkAccessField1-8 8765124 132.1 ns/op
BenchmarkAccessField100-8 8688177 132.2 ns/op
BenchmarkAccessField1-8 8606901 132.8 ns/op
BenchmarkAccessField100-8 8746033 133.2 ns/op
BenchmarkAccessField1-8 8318942 138.6 ns/op
BenchmarkAccessField100-8 8542576 137.2 ns/op
それとも検証状況が悪かったりするのでしょうか。
(例)
- フィールド100個じゃ足りない
- フィールド
int
だと大してパフォに影響でない - インクリメント100回じゃ大して差が出ない
アセンブリコードをみるとなにかわかるやも。(オフセットの有無で命令コードのコストが変わらない?)もしくは別のCPUアーキテクチャ環境だと変化が現れたり。
多分ですけど全部 int 型だと有意差が出にくいんじゃないですかね。
tenntenn さんが示された CL は今の Go コンパイラでは差が出ないと思います。(2019年当時の Go でないと差がでないはず)
以前の Go ではフィールドのオフセット位置を計算する処理が入っていて、対象のフィールドが先頭にある(フィールドが遠いかどうかは関係なく単に先頭)ことでそのコードがインライン化されていたというだけの話になります。今はどちらも同じ INCQ が呼ばれており、コストは同じでした。
この CL が大きく影響したのは当時のコンパイラであるという条件の他に、特に sync.Once の特性上「初回のみ代入されそれ以降は更新されない」というフィールド done がホットパスにある事でコードがインライン化されるという、ちょっと特殊な話でした。
なのでこの件は、僕が書いた fieldalignment の記事とは関係ない話になります。fieldalignment は単純に struct のサイズが小さくなるので、コピーのコストに影響します。(ですので記事内では処理速度には言及していなかったと思います)
皆さまコメントありがとうございます!
特にmattnさん、詳細な仕様まで書いていただけて勉強になりました。
フィールドの位置でパフォーマンスが変わるというのはもう昔の話なのですね。