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

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

このコミットは、Go言語のコンパイラ(具体的には6g、当時の64ビットアーキテクチャ向けコンパイラ)における構造体内の配列のパディングロジックに関する変更です。以前は配列全体のサイズに基づいてパディングが行われていましたが、この変更により、配列の要素のサイズに基づいてパディングが行われるようになります。これにより、メモリのアライメントがより効率的になり、メモリ使用量やアクセス性能の改善が期待されます。

コミット

commit 56003374d348ac367dc1c6852e18f0171697d7d9
Author: Ken Thompson <ken@golang.org>
Date:   Sat Feb 7 13:31:34 2009 -0800

    change array padding in structures
    to pad to size of element rather
    than size of array.
    
    R=r
    OCL=24641
    CL=24641

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

https://github.com/golang/go/commit/56003374d348ac367dc1c6852e18f0171697d7d9

元コミット内容

--- a/src/cmd/6g/align.c
+++ b/src/cmd/6g/align.c
@@ -50,18 +50,28 @@ offmod(Type *t)
 	}
 }
 
+uint32
+arrayelemwidth(Type *t)
+{
+
+	while(t->etype == TARRAY && t->bound >= 0)
+		t = t->type;
+	return t->width;
+}
+
 uint32
 widstruct(Type *t, uint32 o, int flag)
 {
 	Type *f;
-\tint32 w;\n+\tint32 w, m;\n \n 	for(f=t->type; f!=T; f=f->down) {\n \t\tif(f->etype != TFIELD)\n \t\t\tfatal(\"widstruct: not TFIELD: %lT\", f);\n \t\tdowidth(f->type);\n \t\tw = f->type->width;\n-\t\to = rnd(o, w);\n+\t\tm = arrayelemwidth(f->type);\n+\t\to = rnd(o, m);\n \t\tf->width = o;\t// really offset for TFIELD\n \t\to += w;\n \t}\

変更の背景

この変更は、Go言語のコンパイラが構造体内のフィールド、特に配列をメモリ上に配置する際のアライメント(整列)方法を改善するために行われました。

コンピュータのプロセッサは、メモリからデータを読み書きする際に、特定のメモリアドレスにデータが整列されていることを期待します。これを「メモリのアライメント」と呼びます。例えば、4バイトの整数は4の倍数のアドレスに配置されていると、プロセッサは効率的にアクセスできます。もし整列されていない場合、プロセッサは複数回のメモリアクセスを必要としたり、パフォーマンスが低下したり、最悪の場合、ハードウェアエラーを引き起こす可能性があります。

構造体(struct)は、異なる型のフィールドをまとめた複合データ型です。これらのフィールドはメモリ上で連続して配置されますが、アライメント要件を満たすために、フィールド間に「パディング」(詰め物)と呼ばれる未使用のバイトが挿入されることがあります。

このコミット以前のGoコンパイラでは、構造体内の配列のパディングを計算する際に、配列全体のサイズ(f->type->width)を基準にしていました。例えば、[10]byteのような配列の場合、配列全体のサイズは10バイトですが、もし構造体の次のフィールドが4バイトのアライメントを必要とする整数だった場合、10バイトの配列の後に2バイトのパディングが挿入され、次のフィールドが4の倍数のアドレスから始まるように調整されます。

しかし、[10]int32のような配列の場合、int32は4バイトのアライメントを必要とします。配列全体のサイズは40バイトですが、もし配列の要素がそれぞれ4バイトのアライメントを必要とする場合、配列の開始アドレスが4の倍数であれば、各要素も自然と4の倍数のアドレスに配置されます。この場合、配列全体のサイズでパディングを計算すると、不必要なパディングが発生したり、最適なアライメントが得られない可能性がありました。

このコミットの目的は、配列のパディングを配列の「要素のサイズ」に基づいて行うことで、より適切なアライメントを実現し、メモリの利用効率とアクセス性能を向上させることです。特に、配列の要素が小さい型(例: byte, int8)である場合に、この変更が効果を発揮します。

前提知識の解説

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

  • メモリのアライメント (Memory Alignment): プロセッサがメモリからデータを効率的に読み書きするために、データの開始アドレスが特定の倍数になるように配置することです。例えば、32ビット(4バイト)の整数は4の倍数のアドレス(0x00, 0x04, 0x08など)に配置されると、プロセッサは一度のメモリアクセスでその値を読み取ることができます。もし4の倍数でないアドレスに配置されている場合、プロセッサは複数回のメモリアクセスを行ったり、特別な処理を必要としたりするため、パフォーマンスが低下します。

  • パディング (Padding): 構造体やクラスのメンバがメモリ上で適切にアライメントされるように、コンパイラがメンバ間に挿入する未使用のバイトのことです。例えば、以下のような構造体を考えます。

    struct Example {
        char c;    // 1バイト
        int i;     // 4バイト (4バイトアライメントが必要)
        char d;    // 1バイト
    };
    

    この構造体がメモリに配置される際、char cの後にint iが続きますが、int iは4バイトアライメントが必要なため、cの後に3バイトのパディングが挿入され、iが4の倍数のアドレスから始まるように調整されます。

    +---+---+---+---+---+---+---+---+
    | c | P | P | P | i | i | i | i |  (Pはパディング)
    +---+---+---+---+---+---+---+---+
    

    構造体の末尾にも、構造体全体のサイズがアライメント要件を満たすようにパディングが追加されることがあります。

2. Go言語のコンパイラと型システム

  • 6g: Go言語の初期のコンパイラの一つで、64ビットアーキテクチャ(AMD64/x86-64)をターゲットとしていました。Goのコンパイラは、ソースコードを機械語に変換するだけでなく、メモリレイアウトの決定やアライメントの調整も行います。

  • Type 構造体: Goコンパイラの内部で、Go言語の型(整数、文字列、構造体、配列など)を表すために使用されるデータ構造です。このType構造体には、型の種類(etype)、サイズ(width)、配列の場合は要素の型(typeフィールドが指す)や要素数(bound)などの情報が含まれています。

    • t->etype: 型の種類を示す列挙値。例えば、TARRAYは配列型、TFIELDは構造体のフィールドを表します。
    • t->width: その型のメモリ上でのサイズ(バイト単位)を示します。
    • t->type: ポインタや配列の場合、それが指す型や要素の型を表す別のType構造体へのポインタです。
    • t->bound: 配列の場合、その要素数を表します。-1はスライス(可変長配列)を示します。
  • rnd(offset, alignment) 関数: この関数は、与えられたoffsetを、指定されたalignmentの倍数に切り上げる(丸める)ために使用されます。例えば、rnd(5, 4)は8を返します。これは、現在のオフセットが5の場合、次に4バイトアライメントが必要なデータを配置するためには、オフセットを8にする必要があることを意味します。

3. src/cmd/6g/align.c ファイル

このファイルは、Goコンパイラの6g部分におけるメモリのアライメントと構造体のレイアウトに関するロジックを実装しています。widstruct関数は、構造体の各フィールドのオフセットと構造体全体のサイズを計算する主要な関数です。

技術的詳細

このコミットの主要な変更点は、src/cmd/6g/align.cファイルに新しいヘルパー関数arrayelemwidthが追加され、既存のwidstruct関数が修正されたことです。

1. 新しい関数 arrayelemwidth(Type *t) の導入

この関数は、与えられた型tが配列型である場合に、その配列の「最も内側の要素の型」のサイズ(幅)を返します。

  • 目的: 多次元配列(例: [][]int)の場合でも、最終的な要素(この場合はint)のサイズを取得するために使用されます。
  • ロジック:
    uint32
    arrayelemwidth(Type *t)
    {
        while(t->etype == TARRAY && t->bound >= 0) // tが配列型であり、かつ固定長配列である限りループ
            t = t->type; // tを配列の要素の型に更新
        return t->width; // 最終的な要素の型の幅を返す
    }
    
    このwhileループは、tが配列型(TARRAY)であり、かつ固定長配列(t->bound >= 0、スライスではない)である限り、tをその要素の型(t->type)に更新し続けます。これにより、[][]intのような型が渡された場合でも、最終的にint型に到達し、そのint型のwidth(サイズ)が返されます。

2. widstruct 関数の変更

widstruct関数は、構造体tの各フィールドfのオフセットと、構造体全体のサイズを計算する役割を担っています。変更された行は以下の通りです。

-\t\to = rnd(o, w);\n+\t\tm = arrayelemwidth(f->type);\n+\t\to = rnd(o, m);\n```

*   **変更前**:
    `o = rnd(o, w);`
    ここで`w`は`f->type->width`、つまりフィールド`f`の型全体のサイズです。配列の場合、これは配列全体のサイズを意味します。この行は、現在のオフセット`o`を、フィールド`f`の型全体のサイズ`w`の倍数に丸めることで、次のフィールドの開始オフセットを決定していました。これは、配列全体のサイズに基づいてパディングを行っていたことを意味します。

*   **変更後**:
    1.  `m = arrayelemwidth(f->type);`
        新しく追加された`arrayelemwidth`関数を呼び出し、フィールド`f`の型(`f->type`)の最も内側の要素のサイズを取得し、変数`m`に格納します。
    2.  `o = rnd(o, m);`
        現在のオフセット`o`を、`m`(配列の要素のサイズ)の倍数に丸めます。これにより、次のフィールドの開始オフセットが、配列の要素のサイズに基づいて適切にアライメントされるようになります。

この変更により、構造体内の配列フィールドのパディングは、配列全体のサイズではなく、その配列の個々の要素のサイズに基づいて行われるようになります。例えば、`struct { byte b; [4]byte arr; int i; }`のような構造体の場合、`arr`の後に`int i`が続く際に、`arr`の要素である`byte`のサイズ(1バイト)に基づいてパディングが考慮されるようになります。これにより、`int i`が適切に4バイトアライメントされるように、より効率的なパディングが適用されます。

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

変更は`src/cmd/6g/align.c`ファイルに集中しています。

1.  **`arrayelemwidth`関数の追加**:
    ```c
    uint32
    arrayelemwidth(Type *t)
    {
        while(t->etype == TARRAY && t->bound >= 0)
            t = t->type;
        return t->width;
    }
    ```
    この関数は、配列の最も内側の要素の幅を計算します。

2.  **`widstruct`関数の変更**:
    ```diff
    --- a/src/cmd/6g/align.c
    +++ b/src/cmd/6g/align.c
    @@ -50,18 +50,28 @@ offmod(Type *t)
     	}
     }
     
    +uint32
    +arrayelemwidth(Type *t)
    +{
    +
    +	while(t->etype == TARRAY && t->bound >= 0)
    +		t = t->type;
    +	return t->width;
    +}
    +
     uint32
     widstruct(Type *t, uint32 o, int flag)
     {
     	Type *f;
    -\tint32 w;\n+\tint32 w, m;\n \n     	for(f=t->type; f!=T; f=f->down) {\n     	\tif(f->etype != TFIELD)\n     	\t\tfatal(\"widstruct: not TFIELD: %lT\", f);\n     	\tdowidth(f->type);\n     	\tw = f->type->width;\n-\t\to = rnd(o, w);\n+\t\tm = arrayelemwidth(f->type);\n+\t\to = rnd(o, m);\n     	\tf->width = o;\t// really offset for TFIELD\n     	\to += w;\n     	}\
    ```
    `widstruct`関数内で、パディング計算に使用する幅を`f->type->width`から`arrayelemwidth(f->type)`の戻り値に変更しています。また、新しい変数`m`が導入されています。

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

### `arrayelemwidth` 関数

この関数は、Goの型システムにおける配列の構造を深く理解していることを示しています。Goの配列型は、`Type`構造体で表現され、`t->type`フィールドを通じてその要素の型を参照します。多次元配列(例: `[2][3]int`)は、`[2]array_of_3_int`のようにネストされた配列として表現されます。

`arrayelemwidth`関数は、`while`ループを使って、与えられた型`t`が配列型である限り、その要素の型へと`t`を辿っていきます。`t->bound >= 0`という条件は、スライス(可変長配列)ではなく、固定長配列であることを確認しています。スライスは実行時にサイズが決定されるため、コンパイル時のパディング計算には直接関係しません。

最終的にループを抜けたとき、`t`は配列の最も内側の要素の型(例: `int`, `byte`, `struct MyStruct`など)を指しています。そして、その要素の型の`width`(サイズ)が返されます。このサイズが、パディングの基準として使用されます。

### `widstruct` 関数の変更点

`widstruct`関数は、構造体のメモリレイアウトを決定する中心的なロジックを含んでいます。構造体の各フィールド`f`についてループし、そのフィールドのオフセットを計算します。

変更前の`o = rnd(o, w);`では、現在のオフセット`o`を、フィールド`f`の型全体のサイズ`w`(`f->type->width`)の倍数に丸めていました。これは、フィールドが配列の場合、配列全体のサイズに基づいてアライメントを考慮していたことを意味します。

変更後の`m = arrayelemwidth(f->type); o = rnd(o, m);`では、まず`arrayelemwidth`を呼び出して、配列の要素のサイズ`m`を取得します。そして、この`m`を`rnd`関数に渡してパディングを計算します。

**具体例で考える:**

例えば、以下のような構造体を考えます。

```go
type MyStruct struct {
    b byte
    a [4]byte // 4バイトの配列
    i int32   // 4バイト、4バイトアライメントが必要
}
  1. b (byte):

    • oは0から開始。
    • bwidthは1。
    • f->width = o (0)。
    • o += w (0 + 1 = 1)。
  2. a ([4]byte):

    • 現在のoは1。
    • aの型は[4]byte
    • 変更前: w[4]bytewidth、つまり4。o = rnd(1, 4) -> oは4になる。
    • 変更後: m = arrayelemwidth([4]byte) -> bytewidth、つまり1。o = rnd(1, 1) -> oは1のまま。
    • f->width = o (変更前は4、変更後は1)。
    • o += w (変更前は4 + 4 = 8、変更後は1 + 4 = 5)。
  3. i (int32):

    • 現在のoは、変更前は8、変更後は5。
    • iの型はint32int32は4バイトアライメントが必要。
    • 変更前: wint32width、つまり4。o = rnd(8, 4) -> oは8のまま。
    • 変更後: m = arrayelemwidth(int32) -> int32width、つまり4。o = rnd(5, 4) -> oは8になる。
    • f->width = o (変更前は8、変更後は8)。
    • o += w (変更前は8 + 4 = 12、変更後は8 + 4 = 12)。

この例からわかるように、変更前は[4]byteの後にint32が続く際に、[4]byteのサイズ(4)でパディングを考慮していたため、int32はオフセット8から配置されていました。変更後は、[4]byteの要素のサイズ(1)でパディングを考慮するため、[4]byteの直後(オフセット5)からint32を配置しようとしますが、int32が4バイトアライメントを必要とするため、最終的にオフセット8から配置されます。

この特定の例では結果的に同じオフセットになりますが、配列の要素が非常に小さい場合や、構造体のフィールドの並びによっては、この変更がパディングバイトの削減やより効率的なアライメントに繋がります。特に、配列の要素が1バイトで、その後に4バイトアライメントが必要なフィールドが続く場合などに、この変更が効果を発揮します。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/cmd/6g/align.cの周辺コード)
  • メモリのアライメントとパディングに関する一般的なコンピュータサイエンスの知識
  • Go言語の仕様書
  • Go言語の初期のコンパイラ設計に関する情報(必要に応じてWeb検索)