Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16466] ファイルの概要

このコミットは、Go言語のテストファイル test/sizeof.go の修正に関するものです。具体的には、構造体のフィールドオフセットを検証するテストが、amd64アーキテクチャでポインタのアライメントの問題により失敗するのを修正しています。int32型のフィールドをint64型に変更し、それに伴い期待されるオフセット値を更新することで、テストが正しく動作するようにしています。

コミット

commit 2c1acc18f42ffd2412e0e1b9acc04fbc5ea7c0aa
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sun Jun 2 19:10:11 2013 +0200

    test: correct sizeof.go.
    
    It would not pass on amd64 due to alignment of pointers.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/9949043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2c1acc18f42ffd2412e0e1b9acc04fbc5ea7c0aa

元コミット内容

test: correct sizeof.go. It would not pass on amd64 due to alignment of pointers.

このコミットは、test/sizeof.goというテストファイルがamd64アーキテクチャ上でポインタのアライメントの問題により失敗していたのを修正するものです。

変更の背景

Go言語では、構造体内のフィールドはメモリ上で特定のアライメント要件に従って配置されます。これは、CPUが効率的にメモリにアクセスできるようにするためです。特に64ビットアーキテクチャ(amd64など)では、ポインタや64ビット整数型(int64など)は通常8バイト境界にアライメントされます。

元のtest/sizeof.goファイルでは、ネストされた構造体S1からS8が定義されており、それぞれの構造体にはint32型のフィールドが含まれていました。int32は4バイトの型ですが、amd64のような64ビットシステムでは、構造体のアライメントやパディングのルールにより、フィールドのオフセットが期待通りにならない場合があります。特に、構造体内にポインタ型(*S1)が含まれている場合、そのポインタは8バイト境界にアライメントされる必要があり、その前後のフィールドの配置に影響を与えます。

このコミットの背景には、amd64環境でtest/sizeof.goが期待されるオフセット値と異なる結果を返し、テストが失敗するという問題がありました。これは、int32型のフィールドが、amd64のポインタアライメント要件と組み合わさることで、コンパイラが構造体のパディングを挿入し、フィールドのオフセットがずれることが原因でした。

前提知識の解説

1. Go言語の構造体とメモリレイアウト

Go言語の構造体(struct)は、異なる型のフィールドをまとめるための複合データ型です。Goコンパイラは、構造体のフィールドをメモリ上に配置する際に、各フィールドの型のアライメント要件と、構造体全体のアライメント要件を考慮します。

2. メモリのアライメントとパディング

  • アライメント (Alignment): CPUがメモリからデータを読み書きする際に、特定のメモリアドレス境界にデータが配置されている必要があるという要件です。例えば、4バイトの整数は4の倍数のアドレスに、8バイトの整数やポインタは8の倍数のアドレスに配置されるのが一般的です。これにより、CPUは効率的にデータをフェッチできます。アライメントされていないアクセスは、パフォーマンスの低下や、一部のアーキテクチャではエラーを引き起こす可能性があります。
  • パディング (Padding): アライメント要件を満たすために、コンパイラが構造体のフィールド間に挿入する未使用のバイトのことです。例えば、4バイトのフィールドの後に8バイトのフィールドが続く場合、4バイトフィールドの直後に4バイトのパディングが挿入され、8バイトフィールドが8バイト境界に配置されるように調整されることがあります。

3. unsafeパッケージとunsafe.Offsetof

Go言語のunsafeパッケージは、Goの型システムやメモリ安全性の保証をバイパスする低レベルな操作を可能にします。

  • unsafe.Offsetof(x.f): 構造体xのフィールドfが、構造体の先頭から何バイト目に配置されているか(オフセット)を返します。この関数は、メモリレイアウトを直接検査するために使用されます。

4. amd64アーキテクチャ

amd64は、Intel 64(x86-64)とも呼ばれる64ビットのCPUアーキテクチャです。このアーキテクチャでは、ポインタやint64型は通常8バイトのアライメント要件を持ちます。つまり、これらのデータ型は8の倍数のメモリアドレスに配置される必要があります。

5. int32int64のメモリ上の違い

  • int32: 32ビット(4バイト)の符号付き整数型です。
  • int64: 64ビット(8バイト)の符号付き整数型です。

amd64環境では、int32は4バイト境界にアライメントされますが、int64やポインタは8バイト境界にアライメントされます。構造体内にこれらの異なるアライメント要件を持つ型が混在する場合、コンパイラはパディングを挿入してアライメントを調整します。

技術的詳細

このコミットの技術的詳細は、Go言語の構造体におけるメモリレイアウトとアライメントの挙動、特に64ビットアーキテクチャ(amd64)でのポインタのアライメント要件に起因する問題とその解決策にあります。

元のtest/sizeof.goでは、以下のようなネストされた構造体が定義されていました。

type (
	S1 struct {
		A int32
		S2
	}
	S2 struct {
		B int32
		S3
	}
	// ... S3からS7まで同様
	S8 struct {
		H int32
		*S1 // ここにポインタ型がある
	}
)

そして、testDeep()関数内でunsafe.Offsetofを使って各フィールドのオフセットを検証していました。例えば、s1.Bのオフセットが4であることを期待していました。

しかし、amd64アーキテクチャでは、構造体S8に含まれるポインタ型*S1が8バイト境界にアライメントされる必要があります。Goコンパイラは、構造体全体のサイズとアライメントを最適化しようとします。int32(4バイト)のフィールドが連続している場合、理論上は4バイトずつオフセットが増えていくように見えますが、構造体内に8バイトアライメントが必要なフィールド(この場合はポインタ)が存在すると、コンパイラはパディングを挿入してアライメントを調整します。

具体的には、S8H int32の後に*S1が続く場合、Hの後に4バイトのパディングが挿入され、*S1が8バイト境界に配置されるように調整されます。このパディングが、ネストされた構造体全体のオフセット計算に影響を与え、期待されるオフセット値と実際のオフセット値がamd64環境でずれていました。

この問題を解決するために、コミットではint32型のフィールドをすべてint64型に変更しています。

type (
	S1 struct {
		A int64 // int32 -> int64
		S2
	}
	S2 struct {
		B int64 // int32 -> int64
		S3
	}
	// ... S3からS8まで同様
)

int64型はamd64アーキテクチャにおいて8バイトのアライメント要件を持ちます。これにより、各フィールドが最初から8バイト境界に配置されるようになり、ポインタ型*S1のアライメント要件と整合性が取れます。結果として、コンパイラが余分なパディングを挿入する必要がなくなり、フィールドのオフセットが予測可能かつ一貫した8バイトの倍数になります。

例えば、s1.Aがオフセット0に配置されると、s1.Bint64型であるため、次の8バイト境界であるオフセット8に配置されます。同様に、s1.Cはオフセット16に、というように8バイトずつオフセットが増えていきます。これにより、amd64環境でのテストの失敗が解消されます。

また、テストファイルの冒頭のコメントが// compileから// runに変更されています。これは、このファイルが単にコンパイル可能であることを確認するだけでなく、実際に実行してunsafe.Offsetofのチェックが正しく行われることを検証するテストであることを示しています。

コアとなるコードの変更箇所

変更はtest/sizeof.goファイルのみです。

--- a/test/sizeof.go
+++ b/test/sizeof.go
@@ -1,4 +1,4 @@
-// compile
+// run
 
 // Copyright 2011 The Go Authors.  All rights reserved.
 // Use of this source code is governed by a BSD-style
@@ -58,35 +58,35 @@ func main() {
 
 type (
 	S1 struct {
-		A int32
+		A int64
 		S2
 	}
 	S2 struct {
-		B int32
+		B int64
 		S3
 	}
 	S3 struct {
-		C int32
+		C int64
 		S4
 	}
 	S4 struct {
-		D int32
+		D int64
 		S5
 	}
 	S5 struct {
-		E int32
+		E int64
 		S6
 	}
 	S6 struct {
-		F int32
+		F int64
 		S7
 	}
 	S7 struct {
-		G int32
+		G int64
 		S8
 	}
 	S8 struct {
-		H int32
+		H int64
 		*S1
 	}
 )
@@ -96,24 +96,24 @@ func testDeep() {
 	switch {
 	case unsafe.Offsetof(s1.A) != 0:
 		panic("unsafe.Offsetof(s1.A) != 0")
-	case unsafe.Offsetof(s1.B) != 4:
-		panic("unsafe.Offsetof(s1.B) != 4")
-	case unsafe.Offsetof(s1.C) != 8:
-		panic("unsafe.Offsetof(s1.C) != 8")
-	case unsafe.Offsetof(s1.D) != 12:
-		panic("unsafe.Offsetof(s1.D) != 12")
-	case unsafe.Offsetof(s1.E) != 16:
-		panic("unsafe.Offsetof(s1.E) != 16")
-	case unsafe.Offsetof(s1.F) != 20:
-		panic("unsafe.Offsetof(s1.F) != 20")
-	case unsafe.Offsetof(s1.G) != 24:
-		panic("unsafe.Offsetof(s1.G) != 24")
-	case unsafe.Offsetof(s1.H) != 28:
-		panic("unsafe.Offsetof(s1.H) != 28")
-	case unsafe.Offsetof(s1.S1) != 32:
-		panic("unsafe.Offsetof(s1.S1) != 32")
-	case unsafe.Offsetof(s1.S1.S2.S3.S4.S5.S6.S7.S8.S1.S2) != 4:
-		panic("unsafe.Offsetof(s1.S1.S2.S3.S4.S5.S6.S7.S8.S1.S2) != 4")
+	case unsafe.Offsetof(s1.B) != 8:
+		panic("unsafe.Offsetof(s1.B) != 8")
+	case unsafe.Offsetof(s1.C) != 16:
+		panic("unsafe.Offsetof(s1.C) != 16")
+	case unsafe.Offsetof(s1.D) != 24:
+		panic("unsafe.Offsetof(s1.D) != 24")
+	case unsafe.Offsetof(s1.E) != 32:
+		panic("unsafe.Offsetof(s1.E) != 32")
+	case unsafe.Offsetof(s1.F) != 40:
+		panic("unsafe.Offsetof(s1.F) != 40")
+	case unsafe.Offsetof(s1.G) != 48:
+		panic("unsafe.Offsetof(s1.G) != 48")
+	case unsafe.Offsetof(s1.H) != 56:
+		panic("unsafe.Offsetof(s1.H) != 56")
+	case unsafe.Offsetof(s1.S1) != 64:
+		panic("unsafe.Offsetof(s1.S1) != 64")
+	case unsafe.Offsetof(s1.S1.S2.S3.S4.S5.S6.S7.S8.S1.S2) != 8:
+		panic("unsafe.Offsetof(s1.S1.S2.S3.S4.S5.S6.S7.S8.S1.S2) != 8")
 	}\n }\n \n```

## コアとなるコードの解説

このコミットの主要な変更点は以下の2つです。

1.  **テストファイルの実行モードの変更**:
    `// compile` から `// run` へと変更されています。
    *   `// compile`: このディレクティブは、ファイルがGoコンパイラによってエラーなくコンパイルできることをテストします。
    *   `// run`: このディレクティブは、ファイルがコンパイルできるだけでなく、実行時にパニックを起こさずに正常に終了することをテストします。今回のケースでは、`testDeep()`関数内の`panic`文が実行されないことを確認するために必要です。

2.  **構造体フィールドの型変更と期待オフセット値の更新**:
    *   ネストされた構造体`S1`から`S8`までの各フィールド(`A`から`H`)の型が`int32`から`int64`に変更されました。
        *   `int32`は4バイトの整数型です。
        *   `int64`は8バイトの整数型です。
    *   これに伴い、`testDeep()`関数内の`unsafe.Offsetof`によるオフセット検証の期待値が更新されました。
        *   例えば、`unsafe.Offsetof(s1.B)`の期待値は`4`から`8`に変更されました。
        *   `unsafe.Offsetof(s1.C)`の期待値は`8`から`16`に変更されました。
        *   以降のフィールドも同様に、オフセットが4バイトずつではなく8バイトずつ増加するように修正されています。
        *   `unsafe.Offsetof(s1.S1)`の期待値は`32`から`64`に変更されました。これは、`S1`構造体全体が`int64`フィールドとネストされた構造体を含むため、そのサイズとアライメントが8バイトの倍数になることを反映しています。
        *   `unsafe.Offsetof(s1.S1.S2.S3.S4.S5.S6.S7.S8.S1.S2)`のような深くネストされたフィールドのオフセットも、`4`から`8`に変更されています。

これらの変更により、`amd64`アーキテクチャにおけるポインタ(`*S1`)のアライメント要件と、`int64`フィールドの8バイトアライメントが整合し、構造体のパディングが予測可能になります。結果として、`unsafe.Offsetof`が返す値が期待通りになり、テストが正しくパスするようになります。

## 関連リンク

*   Go言語の`unsafe`パッケージに関する公式ドキュメント: [https://pkg.go.dev/unsafe](https://pkg.go.dev/unsafe)
*   Go言語のメモリレイアウトとアライメントに関する議論(GoのIssueやデザインドキュメントなど):
    *   Goの構造体アライメントに関する一般的な情報: [https://go.dev/ref/spec#Struct_types](https://go.dev/ref/spec#Struct_types)
    *   Goのメモリモデル: [https://go.dev/ref/mem](https://go.dev/ref/mem)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント
*   Go言語のソースコード(特に`src/cmd/compile/internal/gc/align.go`など、コンパイラのアライメント処理に関する部分)
*   `amd64`アーキテクチャにおけるメモリのアライメントに関する一般的な情報(CPUアーキテクチャのドキュメントなど)
*   Go言語のIssueトラッカーやメーリングリストでの関連議論
*   Go CL 9949043: [https://golang.org/cl/9949043](https://golang.org/cl/9949043) (コミットメッセージに記載されているChange Listへのリンク)