[インデックス 1913] ファイルの概要
このコミットは、Goコンパイラのメモリ配置とアライメント計算ロジックを、特定のアーキテクチャ(この場合は6g、AMD64向け)のバックエンドから、より汎用的なコンパイラフロントエンド(gc)に移動させる大規模なリファクタリングです。これにより、アライメント計算のコードが中央集約され、異なるアーキテクチャ間での一貫性が確保され、コードの重複が削減されます。
コミット
Author: Russ Cox rsc@golang.org Date: Mon Mar 30 17:09:28 2009 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8e54729b5a9fdbc6cd351d532d5205ab97bcf4dd
元コミット内容
move alignment calculations into gc
R=ken
OCL=26914
CL=26914
変更の背景
Goコンパイラの初期段階では、6g
(AMD64向け)、8g
(x86向け)、5g
(ARM向け) といった各アーキテクチャ固有のバックエンドが、それぞれ独自のメモリ配置やアライメント計算ロジックを持っていました。これは、各バックエンドが独立して開発される初期の段階では許容されるものでしたが、時間の経過とともに、以下のような問題を引き起こす可能性がありました。
- コードの重複: 各バックエンドで同様のアライメント計算ロジックが重複して実装されるため、コード量が増加し、保守が困難になります。
- 一貫性の欠如: 各バックエンドでアライメント計算の実装が異なる場合、異なるアーキテクチャ間でGoプログラムのメモリレイアウトに予期せぬ差異が生じる可能性があります。これは、特にポインタの扱い、構造体のパディング、配列の要素配置などにおいて、バグの原因となることがあります。
- 保守性の低下: アライメントルールやメモリレイアウトに関する変更があった場合、すべてのバックエンドで同様の修正を行う必要があり、手間がかかるだけでなく、修正漏れのリスクも高まります。
このコミットは、これらの問題を解決するために、アライメント計算に関する共通のロジックを、コンパイラのフロントエンドである gc
(generic Go compiler) に移動させることを目的としています。これにより、アライメント計算が一元化され、すべてのバックエンドが共通のロジックを使用するようになります。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラの構造とメモリ管理に関する基本的な概念を理解しておく必要があります。
Goコンパイラの構造 (初期のgc/6g/8g/5g)
Goコンパイラは、大きく分けてフロントエンドとバックエンドに分かれています。
- フロントエンド (
gc
): ソースコードの字句解析、構文解析、型チェック、抽象構文木 (AST) の構築、中間表現 (IR) の生成など、アーキテクチャに依存しない共通の処理を担当します。 - バックエンド (
6g
,8g
,5g
など): フロントエンドが生成した中間表現を受け取り、特定のCPUアーキテクチャ(例:6g
はAMD64)向けの機械語コードを生成します。
このコミット以前は、メモリのアライメント計算の一部がバックエンド (6g/align.c
) に存在していました。
メモリのアライメント (Memory Alignment)
メモリのアライメントとは、コンピュータのメモリ上でデータが配置される際のアドレスの制約のことです。多くのCPUアーキテクチャでは、特定のデータ型(例: 4バイト整数、8バイトポインタ)は、そのデータ型のサイズと同じ倍数のアドレスに配置されると、最も効率的にアクセスできます。
- アライメントの重要性:
- パフォーマンス: CPUは、アライメントされたデータにアクセスする方が、アライメントされていないデータにアクセスするよりも高速です。アライメントされていないアクセスは、追加のCPUサイクルを必要としたり、場合によってはハードウェア例外を引き起こしたりすることがあります。
- 正確性: 一部のハードウェアは、アライメントされていないメモリアクセスを許可せず、プログラムのクラッシュや予期せぬ動作を引き起こす可能性があります。
- データ構造のパディング: 構造体 (struct) のフィールドは、アライメント要件を満たすために、フィールド間に未使用のメモリ領域(パディング)が挿入されることがあります。これにより、構造体の実際のサイズが、個々のフィールドのサイズの合計よりも大きくなることがあります。
Goコンパイラにおける型システムとサイズ計算
Goコンパイラは、プログラム内のすべての型について、そのサイズ(メモリをどれだけ占有するか)とアライメント要件を計算する必要があります。これは、メモリ割り当て、構造体のレイアウト、関数呼び出し時の引数と戻り値のスタックフレームの構築などに不可欠です。
Type
構造体: コンパイラ内部では、Goの各型はType
構造体で表現されます。この構造体には、型の種類 (etype
)、サイズ (width
)、フィールド情報などが含まれます。dowidth
関数: この関数は、与えられたType
のメモリ上のサイズ (width
) を計算する主要な関数です。構造体や配列など、複合型のサイズを再帰的に計算します。rnd
関数:rnd(offset, alignment)
は、offset
をalignment
の倍数に切り上げる(アライメントする)ために使用される関数です。これは、構造体のフィールドオフセットや、全体のサイズを計算する際に頻繁に利用されます。
技術的詳細
このコミットの技術的な核心は、アライメント計算ロジックの物理的な移動と、それに伴うコンパイラの初期化フローの変更です。
-
align.c
の移動と統合:src/cmd/6g/align.c
が削除され、その内容(rnd
,offmod
,arrayelemwidth
,widstruct
,dowidth
といったアライメント計算関連の関数)がsrc/cmd/gc/align.c
という新しいファイルとして作成されました。- これにより、これらの関数は
6g
だけでなく、他のすべてのバックエンドからも利用可能な共通のコンポーネントとなりました。
-
コンパイラ初期化フローの変更:
- 以前は、
6g
のbelexinit
関数が、型定義の初期化とアライメント関連のグローバル変数(wptr
,wmax
)の設定を行っていました。 - このコミットでは、
src/cmd/gc/lex.c
のmain
関数とlexinit
関数が変更され、新たにbetypeinit()
とtypeinit(LBASETYPE)
が呼び出されるようになりました。betypeinit()
:maxround
(最大アライメント境界) とwidthptr
(ポインタの幅) といった、アーキテクチャに依存するがコンパイラ全体で共通して使用されるアライメント関連のグローバル変数を初期化します。これらは以前6g/align.c
で静的に定義されていました。typeinit()
:simtype
(型の単純化マッピング)、isint
(整数型かどうかのフラグ)、isfloat
(浮動小数点型かどうかのフラグ) などの基本的な型プロパティを初期化します。また、TPTR32
やTPTR64
といったポインタ型のサイズ計算もここで行われます。さらに、Goの組み込み型であるスライス (Array
構造体) の内部オフセット (Array_array
,Array_nel
,Array_cap
) と全体のサイズ (sizeof_Array
) もtypeinit
内で計算されるようになりました。これにより、スライスのメモリレイアウトがコンパイラ全体で一貫して定義されます。
- 以前は、
-
グローバル変数の昇格:
maxround
とwidthptr
は、以前は6g/align.c
内の静的変数でしたが、src/cmd/gc/go.h
でEXTERN
宣言され、gc
全体でアクセス可能なグローバル変数となりました。これは、これらの値がコンパイラの共通部分で利用されるようになったことを示しています。
-
Typedef
構造体の定義と利用:src/cmd/gc/go.h
にTypedef
構造体が定義され、typedefs
というグローバル配列が宣言されました。これは、Goの組み込み型エイリアス(int
,uint
,uintptr
,float
)の定義を構造化するために使用されます。typeinit
関数内でこのtypedefs
配列をループ処理し、各エイリアス型に対応するType
構造体を初期化し、そのサイズをdowidth
で計算しています。
-
エラーチェックの追加:
gc/align.c
のrnd
およびdowidth
関数に、maxround == 0
やwidthptr == 0
のチェックが追加されました。これは、betypeinit
が適切に呼び出され、これらの重要なアライメント関連のグローバル変数が初期化されていることを保証するためのものです。初期化されていない場合、fatal
エラーでコンパイラが終了します。
-
src/cmd/gc/walk.c
の変更:ascompatte
関数内で、異なるアライメントを持つ構造体に関するエラーチェックが追加されました。これは、アライメント計算ロジックの移動に伴い、潜在的な不整合を早期に検出するための防御的な変更と考えられます。
これらの変更により、Goコンパイラは、メモリのアライメントと型サイズの計算に関して、より堅牢で一貫性のある基盤を持つことになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/cmd/6g/align.c
: ほとんどのコードが削除されました。rnd
,offmod
,arrayelemwidth
,widstruct
,dowidth
,besetptr
,belexinit
といった関数が削除されています。src/cmd/6g/gg.h
:Array_array
,Array_nel
,Array_cap
,sizeof_Array
といった配列のランタイム表現に関するEXTERN
宣言、およびbelexinit
関数のプロトタイプが削除されました。src/cmd/gc/Makefile
:align.c
がOFILES
に追加され、コンパイル対象となりました。src/cmd/gc/align.c
: 新規作成されたファイルで、6g/align.c
から移動されたアライメント計算ロジック(rnd
,offmod
,arrayelemwidth
,widstruct
,dowidth
)が含まれています。また、typeinit
とbetypeinit
という新しい初期化関数が追加されました。src/cmd/gc/go.h
:Typedef
構造体とtypedefs
グローバル配列の宣言が追加されました。Array_array
,Array_nel
,Array_cap
,sizeof_Array
といった配列のランタイム表現に関するEXTERN
宣言が追加されました。maxround
とwidthptr
というアライメント関連のグローバル変数のEXTERN
宣言が追加されました。mainlex
,belexinit
,besetptr
の関数プロトタイプが削除され、typeinit
,betypeinit
のプロトタイプが追加されました。
src/cmd/gc/lex.c
:mainlex
関数がmain
にリネームされ、コンパイラのメインエントリポイントとなりました。main
関数内でbetypeinit()
とtypeinit(LBASETYPE)
が呼び出されるようになりました。lexinit
関数から、以前belexinit
やbesetptr
で行われていた型初期化とアライメント関連の処理が削除されました。
src/cmd/gc/walk.c
:ascompatte
関数内で、構造体のアライメントに関するエラーチェックのロギングとエラーメッセージが変更されました。
コアとなるコードの解説
src/cmd/gc/align.c
(新規ファイル)
このファイルは、Goコンパイラの型サイズとメモリレイアウトを決定する中心的なロジックを含んでいます。
-
uint32 rnd(uint32 o, uint32 r)
:uint32 rnd(uint32 o, uint32 r) { if(maxround == 0) fatal("rnd"); // maxroundが初期化されていない場合は致命的エラー if(r > maxround) r = maxround; // アライメント要求が最大アライメント境界を超える場合は、最大境界に制限 if(r != 0) while(o%r != 0) o++; // oがrの倍数になるまでインクリメント return o; }
この関数は、オフセット
o
を指定されたアライメントr
の倍数に切り上げるために使用されます。例えば、rnd(5, 4)
は8
を返します。これは、構造体内のフィールドのオフセットや、構造体全体のサイズを計算する際に、アライメント要件を満たすためにパディングを挿入する役割を果たします。 -
void dowidth(Type *t)
:void dowidth(Type *t) { // ... (省略) ... if(maxround == 0 || widthptr == 0) fatal("dowidth without betypeinit"); // 重要なグローバル変数が初期化されているかチェック // ... (省略) ... if(t->width == -2) { yyerror("invalid recursive type %T", t); // 再帰的な型定義の検出 t->width = 0; return; } t->width = -2; // 処理中であることを示すマーカー // ... (型の種類に応じたサイズ計算ロジック) ... t->width = w; // 計算されたサイズをType構造体に設定 }
dowidth
は、Goの型t
のメモリ上のサイズ (t->width
) を計算する主要な関数です。構造体、配列、ポインタ、プリミティブ型など、あらゆる種類の型に対応しています。再帰的な型定義(例: 自身をフィールドに持つ構造体)を検出するためのt->width = -2
というマーカーを使用しています。この関数は、rnd
やwidstruct
などのヘルパー関数を呼び出して、正確なサイズとアライメントを決定します。 -
static uint32 widstruct(Type *t, uint32 o, int flag)
:static uint32 widstruct(Type *t, uint32 o, int flag) { // ... (省略) ... for(f=t->type; f!=T; f=f->down) { // 構造体の各フィールドをイテレート // ... (フィールドの型サイズとアライメント要件を取得) ... o = rnd(o, m); // 現在のオフセットをフィールドのアライメント要件に合わせて調整 f->width = o; // フィールドのオフセットを記録 o += w; // フィールドのサイズ分オフセットを進める } if(flag) o = rnd(o, maxround); // 構造体全体のサイズを最大アライメント境界に合わせて調整 // ... (構造体の幅を計算) ... return o; }
この関数は、構造体
t
のメモリ上の幅(サイズ)を計算します。各フィールドのサイズとアライメント要件を考慮し、必要に応じてパディングを挿入しながらオフセットo
を更新していきます。rnd
関数を呼び出して、各フィールドが適切にアライメントされるようにします。 -
void betypeinit(void)
:void betypeinit(void) { maxround = 8; // 最大アライメント境界を8バイトに設定 widthptr = 8; // ポインタの幅を8バイトに設定 (AMD64の場合) }
この関数は、アーキテクチャに依存するがコンパイラ全体で共通して使用されるアライメント関連のグローバル変数
maxround
とwidthptr
を初期化します。これらは、Goコンパイラがターゲットアーキテクチャのメモリレイアウトを理解するために不可欠な値です。 -
void typeinit(int lex)
:void typeinit(int lex) { // ... (省略) ... if(widthptr == 0) fatal("typeinit before betypeinit"); // betypeinitが呼び出されているかチェック // ... (各種型プロパティの初期化) ... // ポインタ型のサイズ計算 types[TPTR32] = typ(TPTR32); dowidth(types[TPTR32]); types[TPTR64] = typ(TPTR64); dowidth(types[TPTR64]); tptr = TPTR32; if(widthptr == 8) tptr = TPTR64; // ... (整数型、浮動小数点型、符号付き型などのフラグ設定) ... // 組み込み型エイリアスの初期化とサイズ計算 for(i=0; typedefs[i].name; i++) { // ... (typedefs配列から型情報を取得し、Type構造体を初期化) ... dowidth(t); // 各エイリアス型のサイズを計算 // ... (省略) ... } // Goスライス (Array) のランタイム表現のオフセットとサイズ計算 Array_array = rnd(0, widthptr); Array_nel = rnd(Array_array+widthptr, types[TUINT32]->width); Array_cap = rnd(Array_nel+types[TUINT32]->width, types[TUINT32]->width); sizeof_Array = rnd(Array_cap+types[TUINT32]->width, maxround); }
typeinit
は、コンパイラの型システム全体を初期化する重要な関数です。betypeinit
が呼び出された後に実行され、基本的な型プロパティ(整数型、浮動小数点型、ポインタ型など)のフラグを設定します。特に注目すべきは、typedefs
配列を処理してGoの組み込み型エイリアス(int
,uint
,uintptr
,float
)のサイズをdowidth
を使って計算している点です。さらに、Goのスライスがランタイムでどのようにメモリに配置されるか(データポインタ、要素数、容量のオフセット)を計算し、Array_array
,Array_nel
,Array_cap
,sizeof_Array
といったグローバル変数に設定しています。これにより、スライスのメモリレイアウトがコンパイラ全体で一貫して扱われるようになります。
src/cmd/gc/lex.c
int main(int argc, char *argv[])
:
コンパイラのメインエントリポイントであるint main(int argc, char *argv[]) { // ... (省略) ... betypeinit(); // アライメント関連のグローバル変数を初期化 if(maxround == 0 || widthptr == 0) fatal("betypeinit failed"); // 初期化失敗チェック lexinit(); // 字句解析器の初期化 typeinit(LBASETYPE); // 型システムの初期化 // ... (省略) ... }
main
関数内で、betypeinit
とtypeinit
が明示的に呼び出されるようになりました。これにより、コンパイル処理の早い段階で、すべての型サイズとアライメント情報が適切に設定されることが保証されます。
src/cmd/gc/go.h
-
グローバル変数の宣言:
EXTERN int maxround; EXTERN int widthptr;
maxround
とwidthptr
がEXTERN
宣言されたことで、これらの変数がgc
パッケージ全体で共有されるグローバル変数であることが明確になりました。これは、アライメント計算が特定のバックエンドに閉じず、コンパイラ全体で共通のパラメータを使用することを示す重要な変更です。 -
Array
構造体オフセットの宣言:EXTERN int Array_array; // runtime offsetof(Array,array) EXTERN int Array_nel; // runtime offsetof(Array,nel) EXTERN int Array_cap; // runtime offsetof(Array,cap) EXTERN int sizeof_Array; // runtime sizeof(Array)
Goのスライスがランタイムでどのように表現されるかを示すこれらのオフセットも、
gc
の共通ヘッダーファイルに移動されました。これにより、スライスの内部構造に関する情報が中央集約され、コンパイラの異なる部分や、将来的に異なるバックエンドがこの共通の定義を利用できるようになります。
関連リンク
- Goコンパイラの内部構造に関する一般的な情報: https://go.dev/doc/compiler (これは現代のGoコンパイラに関するドキュメントですが、基本的な概念は共通しています)
- Goのメモリレイアウトとアライメントに関する議論 (より現代のGoに関するものが多いですが、概念は適用可能です):
参考にした情報源リンク
- Go言語の公式Gitリポジトリ: https://github.com/golang/go
- コミットハッシュ
8e54729b5a9fdbc6cd351d532d5205ab97bcf4dd
の詳細ページ: https://github.com/golang/go/commit/8e54729b5a9fdbc6cd351d532d5205ab97bcf4dd - Goコンパイラのソースコード (特に
src/cmd/gc
およびsrc/cmd/6g
ディレクトリの初期のバージョン)