15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】構造体のフィールド順序はパフォーマンスにどう影響するか?~ 検証レポート ~

15
Posted at

はじめに

Goコンパイラは構造体の各フィールドを、そのフィールド型のアライメント要件に従ってメモリ上に配置する。

  • CやRustとは異なり、コンパイラがフィールドの自動並び替えを行わない。
  • 宣言順序がそのままメモリレイアウトに反映される。

Go言語には上記のような前提があります。

今回、「フィールド宣言順序の違い」がパフォーマンスにどの程度の影響を与えるのか気になったので調査を行ってみました。

本記事ではそのレポート結果を記事にしましたので、ぜひご覧いただければ。

1. 目的と背景

背景

冒頭にも記載しましたが...。

Goコンパイラは構造体の各フィールドを、そのフィールド型のアライメント要件に従ってメモリ上に配置します。

コンパイル時に自動並び替え(reordering)は行われません。
そのため、宣言順序がそのままメモリレイアウトに反映されます。

64-bitアーキテクチャにおけるアライメント規則

型によってアラインメントの規則があるようです。

サイズ アライメント
bool / int8 / byte 1 B 1
int16 2 B 2
int32 / float32 4 B 4
int64 / float64 / pointer 8 B 8
string 16 B 8
slice 24 B 8

フィールド間にはアライメントを満たすためのパディング(未使用バイト)が挿入され、構造体全体のサイズは最大アライメントの倍数に切り上げられます。

このあたりは以下の記事が参考になります。

目的

今回の目的を定義しますね。

フィールド宣言順序の違いが、構造体サイズ・ヒープアロケーションコスト・ベンチマーク性能(ns/op)に与える影響を定量的に測定する。

2. 「仮説」と「期待値」

仮説

では早速検証しようと思いますが。その前に自身の仮説をお聞きいただきたく。

本検証では、以下の2つのサンプルで検証しようと思います。

Bad Pattern: アライメントを無視した順序(小→大→小の交互配置)

type Unpadded struct {
    a bool    // 1 B + 7 B padding
    b int64   // 8 B
    c bool    // 1 B + 3 B padding
    d int32   // 4 B
}             // Total: 24 B

Good Pattern — サイズ降順に配置(パディング最小化)

type Padded struct {
    b int64   // 8 B
    d int32   // 4 B
    a bool    // 1 B
    c bool    // 1 B + 2 B padding (構造体末尾アライメント)
}             // Total: 16 B

期待値

Metric Bad Good Diff
サイズ 24 B 16 B -33%
Allocs/op (N個生成) 同等 同等 -
ns/op (N個生成) 高い 低い -

※ サイズ差はunsafe.Sizeofで静的に確認可能ですので、それをもとに期待値を計算しております。

3. 検証

検証環境

  • Go: 1.25.7
  • OS/Arch: darwin/arm64
  • CPU: Apple M2

方法

  1. 静的サイズ検証: unsafe.Sizeofunsafe.Offsetof を用いて、実際のメモリレイアウトを出力
  2. ベンチマーク: testing.B を使用し、以下の2点を測定
  • BenchmarkAlloc: 100万件の要素を持つスライスを make する速度(アロケーション性能)
  • BenchmarkTraverse: 100万件の構造体スライスを全走査して値を集計する速度(キャッシュライン効率)

検証コード

1. サンプル

以下、2つの構造体を検証の対象とします。

2. 静的サイズ検証

以下テストコードでの検証を行います。
24 B, 16 B がそれぞれログ出力されれば、仮説立証ですね。

3. アロケーション性能調査

以下、該当の構造体を対象にmakeでの検証を行います。

4. キャッシュ効率調査

4. 結果

4.1 静的サイズ検証結果

=== Unpadded ===
  a bool   offset=0   size=1   (+7B padding)
  b int64  offset=8   size=8
  c bool   offset=16  size=1   (+3B padding)
  d int32  offset=20  size=4
  total=24 B

=== Padded ===
  b int64  offset=0   size=8
  d int32  offset=8   size=4
  a bool   offset=12  size=1
  c bool   offset=13  size=1   (+2B tail padding)
  total=16 B

仮説通り、定義順序を変えるだけで 24B → 16B (33.3%削減) となりました。

4.2 ベンチマーク結果

go test -bench . -benchmem -count 5の実行結果です。

goos: darwin
goarch: arm64
pkg: go-lab/experiments/struct-padding
cpu: Apple M2
BenchmarkAllocUnpadded-8        3421    350996 ns/op   25165846 B/op    1 allocs/op
BenchmarkAllocUnpadded-8        3373    353656 ns/op   25165841 B/op    1 allocs/op
BenchmarkAllocUnpadded-8        3466    353864 ns/op   25165840 B/op    1 allocs/op
BenchmarkAllocUnpadded-8        3434    359303 ns/op   25165840 B/op    1 allocs/op
BenchmarkAllocUnpadded-8        3483    348602 ns/op   25165841 B/op    1 allocs/op
BenchmarkAllocPadded-8          4838    257856 ns/op   16777232 B/op    1 allocs/op
BenchmarkAllocPadded-8          4764    258257 ns/op   16777232 B/op    1 allocs/op
BenchmarkAllocPadded-8          4456    258710 ns/op   16777231 B/op    1 allocs/op
BenchmarkAllocPadded-8          4802    256633 ns/op   16777231 B/op    1 allocs/op
BenchmarkAllocPadded-8          4714    260774 ns/op   16777232 B/op    1 allocs/op
BenchmarkTraverseUnpadded-8     1621    721990 ns/op          0 B/op    0 allocs/op
BenchmarkTraverseUnpadded-8     1662    731117 ns/op          0 B/op    0 allocs/op
BenchmarkTraverseUnpadded-8     1688    726764 ns/op          0 B/op    0 allocs/op
BenchmarkTraverseUnpadded-8     1653    715831 ns/op          0 B/op    0 allocs/op
BenchmarkTraverseUnpadded-8     1585    724513 ns/op          0 B/op    0 allocs/op
BenchmarkTraversePadded-8       1868    636454 ns/op          0 B/op    0 allocs/op
BenchmarkTraversePadded-8       1812    646448 ns/op          0 B/op    0 allocs/op
BenchmarkTraversePadded-8       1920    632921 ns/op          0 B/op    0 allocs/op
BenchmarkTraversePadded-8       1932    639603 ns/op          0 B/op    0 allocs/op
BenchmarkTraversePadded-8       1802    636722 ns/op          0 B/op    0 allocs/op
PASS
ok      go-lab/experiments/struct-padding    24.309s

分析

結果を表にまとめますね。

Metric Unpadded (Bad) Padded (Good) Diff
unsafe.Sizeof 24 B 16 B -33.3%
Alloc B/op (1M) 25,165,841 B 16,777,232 B -33.3%
Alloc ns/op (1M) ~353,284 ns ~258,446 ns -26.8%
Traverse ns/op (1M) ~724,043 ns ~638,430 ns -11.8%
Allocs/op 1 1 0%
  1. サイズ差 (-33.3%):
    仮説通り、unsafe.Sizeof で Unpadded=24B / Padded=16B を確認できました。
    フィールド間パディングが計 8B 削減されました。
  2. アロケーションコスト (-26.8%):
    1M要素スライスの make で、総バイト数が 33% 減少したことに比例してアロケーション時間も短縮。
    メモリアロケータの作業量がバイト数に依存することを示しています。
  3. 走査スループット (-11.8%):
    同一キャッシュライン (64B) に収容できる要素数が Unpadded=2.67個 vs Padded=4個 となり、キャッシュミス率の低下が走査速度に反映されました。
    仮説の予測範囲(-10〜20%)内ですね。
  4. Allocs/op は同等:
    make([]T, N) は型サイズに関係なく 1 alloc。
    構造体サイズはアロケーション回数ではなくバイト量にのみ影響しています。

5. 結論

仮説が立証できました!!

具体的には、
Go言語において構造体のフィールド定義順序を最適化(サイズ降順など)することで、以下のメリットがあることを実測値として確認できました。

  • 構造体サイズが減る (今回は33%削減)
  • 大量生成時の処理速度が向上 (今回は約27%高速化)
  • スライス走査などの計算処理も高速化する (今回はキャッシュ効率向上により約12%高速化)

また、冒頭の前提に記載した、「Goコンパイラがフィールド自動並び替えを行わない」ことを本検証で確認できたとともに、手動最適化の余地を生むことを実測データで確認できました。

まとめ

実際の現場では、構造体フィールドの極端な順序変更は可読性と引き換えになってしまうこともあるかと。
今回はあくまで影響がどの程度あるのかな、と計測してみた結果の共有までです。

全ての構造体をサイズ順に並べるべきとは限りませんが、大量のデータを扱う構造体においては最適化の余地があるとわかってよかったなと。

時間があれば、
より複雑な構造体(string, slice, pointer 混在)等でのパディング影響測定や、構造体の入れ子が本テーマにおいてどんな影響を与えるか、など試してみたいなとも思います。


ちなみにですが、順序変更については以下のようなツールがあります。

参考

15
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?