Slide 1

Slide 1 text

fieldalignmentから見るGoの構造体 2025/04/24 Monthly LT Go kuroda naoki 1 / 19

Slide 2

Slide 2 text

目的 1. fieldalignmentの実装を眺めて最適な構造体の配置を 理解する。 2. フォールスシェアリングを理解する。 2 / 19

Slide 3

Slide 3 text

公式のGo Toolsの1つ 構造体のフィールドのメモリ効率の良い配置を提案する。 go vet や gopls には入っていない。 他のアナライザーのように明らかなミスを報告するわけではないため。 以下のようにインストールして実行。 go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest fieldalignment . 出力例: struct with 24 bytes could be 16 bytes fieldalignmentとは 3 / 19

Slide 4

Slide 4 text

// before type PoorlyAligned struct { a byte // 1バイト b int64 // 8バイト (7バイトのパディングが必要) c byte // 1バイト } // 合計: 24バイト (実データ10バイト + パディング14バイト) // after type WellAligned struct { b int64 // 8バイト a byte // 1バイト c byte // 1バイト // 6バイトのパディング } // 合計: 16バイト (実データ10バイト + パディング6バイト) fieldalignmentの提案 4 / 19

Slide 5

Slide 5 text

特定の型のデータを特定の区切りの良いアドレスに配置すること その区切りの良いアラインメントを実現するためにコンパイラがパディング(隙間の バイト)を挿入する 各データ型には「アラインメント要件」がある int64 (8バイト): 8バイト境界 int32 (4バイト): 4バイト境界 int16 (2バイト): 2バイト境界 byte / bool (1バイト): 1バイト境界 メモリアラインメントの基本 5 / 19

Slide 6

Slide 6 text

func optimalOrder(str *types.Struct, sizes *gcSizes) (*types.Struct, []int) { nf := str.NumFields() type elem struct { index int alignof int64 sizeof int64 ptrdata int64 } elems := make([]elem, nf) for i := range nf { field := str.Field(i) ft := field.Type() elems[i] = elem{ i, sizes.Alignof(ft), sizes.Sizeof(ft), sizes.ptrdata(ft), } } fieldalignmentの具体的な実装 6 / 19

Slide 7

Slide 7 text

sort.Slice(elems, func(i, j int) bool { ei := &elems[i] ej := &elems[j] // 1. サイズが0のフィールドを先に配置 // Place zero sized objects before non-zero sized objects. zeroi := ei.sizeof == 0 zeroj := ej.sizeof == 0 if zeroi != zeroj { return zeroi } // 2. アラインメント要件の大きいフィールドを先に配置 // Next, place more tightly aligned objects before less tightly aligned objects. if ei.alignof != ej.alignof { return ei.alignof > ej.alignof } // 3. ポインタを含むフィールドを先に配置 // Place pointerful objects before pointer-free objects. noptrsi := ei.ptrdata == 0 noptrsj := ej.ptrdata == 0 if noptrsi != noptrsj { return noptrsj } 7 / 19

Slide 8

Slide 8 text

// 4. ポインタを含む場合、末尾の非ポインタ部分が小さいものを先に if !noptrsi { // If both have pointers... // ... then place objects with less trailing // non-pointer bytes earlier. That is, place // the field with the most trailing // non-pointer bytes at the end of the // pointerful section. traili := ei.sizeof - ei.ptrdata trailj := ej.sizeof - ej.ptrdata if traili != trailj { return traili < trailj } } // 5. サイズの大きいフィールドを先に配置 // Lastly, order by size. if ei.sizeof != ej.sizeof { return ei.sizeof > ej.sizeof } return false }) fields := make([]*types.Var, nf) indexes := make([]int, nf) for i, e := range elems { fields[i] = str.Field(e.index) indexes[i] = e.index } return types.NewStruct(fields, nil), indexes } 8 / 19

Slide 9

Slide 9 text

1. アラインメント要件の大きいフィールドを先に配置 2. ポインタを含むフィールドを先に配置 (構造体内でポインタを持つフィールドを前方 に集めることで、GCがポインタを探すためにス キャンしなければならない範囲をできるだけ小 さくする。 ) fieldalignmentの配置の原則 9 / 19

Slide 10

Slide 10 text

fieldalignmentのgodocにも This analyzer find structs that can be rearranged to use less memory, and provides a suggested edit with the most compact order. Note that there are two different diagnostics reported. One checks struct size, and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the object that the garbage collector has to potentially scan for pointers とある。 struct size pointer bytes の2つを元に配置している。 fieldalignmentの配置の原則 10 / 19

Slide 11

Slide 11 text

文字列( string ) - 内部的にはポインタと長さの組み合わせ スライス( []T ) - 配列に対するポインタ マップ( map[K]V ) チャネル( chan T ) 関数( func() ) インターフェース( interface{} ) 明示的なポインタ ( *T ) ポインタを含むフィールドの例 11 / 19

Slide 12

Slide 12 text

またfieldalignmentのgodocの続きには以下のようなことがある。 Be aware that the most compact order is not always the most efficient. In rare cases it may cause two variables each updated by its own goroutine to occupy the same CPU cache line, inducing a form of memory contention known as "false sharing" that slows down both goroutines. fieldalignmentで配置を変更すると、フォールスシェアリングの可能性がある。 フォールスシェアリング(偽共有)の可能性 12 / 19

Slide 13

Slide 13 text

複数のgoroutineが同じCPUキャッシュライン(CPUがメインメモリからデータを読 み込む際にキャッシュ上にロードする単位)上の異なるフィールドに同時アクセスす る問題 CPUは構造体の特定のフィールドだけでなく、そのフィールドの周辺のフィールドも キャッシュにロードする。 他のgoroutineは同じキャッシュライン内の別のデータにアクセスする際に再読み込 みが必要 結果として予期せぬパフォーマンス低下が発生 フォールスシェアリング(偽共有)とは? 13 / 19

Slide 14

Slide 14 text

1. 複数のcoreが同じキャッシュラインを共有 main memory a b ..... other variables キャッシュにロード core1 の cache a b ..... core2 の cache a b ..... Core1 Core2 フォールスシェアリング(偽共有)とは? 14 / 19

Slide 15

Slide 15 text

2. 1つのcoreがキャッシュラインのaを更新 main memory a b ..... other variables core1's cache a b ..... 更新 書き込み core2's cache a b ..... Core1 Core2 フォールスシェアリング(偽共有)とは? 15 / 19

Slide 16

Slide 16 text

3. 別のcoreがキャッシュラインのbを読み込む この時に同じキャッシュラインを共有しているaが更新されたので、一貫性(キ ャッシュコヒーレンシ)を保つためにメインメモリから再度読み込む main memory a b ..... other variables キャッシュライン にロード core1 の cache a b ..... core2 の cache a b ..... 読み込み Core1 Core2 フォールスシェアリング(偽共有)とは? 16 / 19

Slide 17

Slide 17 text

// フォールスシェアリングを起こす可能性のある構造体 type SharedCounterBad struct { a int64 // ゴルーチンAが更新 b int64 // ゴルーチンBが更新 } // パディングによる対策 type SharedCounterGood struct { a int64 _ [56]byte // パディング (64バイト - 8バイト) b int64 } フォールスシェアリングの対策 17 / 19

Slide 18

Slide 18 text

fieldalignment は構造体のフィールドを、メモリにとって 最適な配置の形に提案してくれる。 メモリアラインメントとポインタを考慮している。 ただし、フォールスシェアリングの可能性もあるので注 意。 まとめ 18 / 19

Slide 19

Slide 19 text

fieldalignment https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment Big Sky :: Go の struct は小さくできる(fieldalignment のススメ) https://mattn.kaoriya.net/software/lang/go/20220127151742.htm "Why Memory Alignment Matters in Go: Making Your Structs Lean and Fast" by Anh Tu Nguyen https://dev.to/tuna99/why-memory-alignment-matters-in-go-making-your- structs-lean-and-fast-1kfk What’s false sharing and how to solve it (using Golang as example) by Genchi Lu https://medium.com/p/whats-false-sharing-and-how-to-solve-it-using-golang- as-example-ef978a305e10?source=social.tw 参考 19 / 19