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

[インデックス 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向け) といった各アーキテクチャ固有のバックエンドが、それぞれ独自のメモリ配置やアライメント計算ロジックを持っていました。これは、各バックエンドが独立して開発される初期の段階では許容されるものでしたが、時間の経過とともに、以下のような問題を引き起こす可能性がありました。

  1. コードの重複: 各バックエンドで同様のアライメント計算ロジックが重複して実装されるため、コード量が増加し、保守が困難になります。
  2. 一貫性の欠如: 各バックエンドでアライメント計算の実装が異なる場合、異なるアーキテクチャ間でGoプログラムのメモリレイアウトに予期せぬ差異が生じる可能性があります。これは、特にポインタの扱い、構造体のパディング、配列の要素配置などにおいて、バグの原因となることがあります。
  3. 保守性の低下: アライメントルールやメモリレイアウトに関する変更があった場合、すべてのバックエンドで同様の修正を行う必要があり、手間がかかるだけでなく、修正漏れのリスクも高まります。

このコミットは、これらの問題を解決するために、アライメント計算に関する共通のロジックを、コンパイラのフロントエンドである 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) は、offsetalignment の倍数に切り上げる(アライメントする)ために使用される関数です。これは、構造体のフィールドオフセットや、全体のサイズを計算する際に頻繁に利用されます。

技術的詳細

このコミットの技術的な核心は、アライメント計算ロジックの物理的な移動と、それに伴うコンパイラの初期化フローの変更です。

  1. align.c の移動と統合:

    • src/cmd/6g/align.c が削除され、その内容(rnd, offmod, arrayelemwidth, widstruct, dowidth といったアライメント計算関連の関数)が src/cmd/gc/align.c という新しいファイルとして作成されました。
    • これにより、これらの関数は 6g だけでなく、他のすべてのバックエンドからも利用可能な共通のコンポーネントとなりました。
  2. コンパイラ初期化フローの変更:

    • 以前は、6gbelexinit 関数が、型定義の初期化とアライメント関連のグローバル変数(wptr, wmax)の設定を行っていました。
    • このコミットでは、src/cmd/gc/lex.cmain 関数と lexinit 関数が変更され、新たに betypeinit()typeinit(LBASETYPE) が呼び出されるようになりました。
      • betypeinit(): maxround (最大アライメント境界) と widthptr (ポインタの幅) といった、アーキテクチャに依存するがコンパイラ全体で共通して使用されるアライメント関連のグローバル変数を初期化します。これらは以前 6g/align.c で静的に定義されていました。
      • typeinit(): simtype (型の単純化マッピング)、isint (整数型かどうかのフラグ)、isfloat (浮動小数点型かどうかのフラグ) などの基本的な型プロパティを初期化します。また、TPTR32TPTR64 といったポインタ型のサイズ計算もここで行われます。さらに、Goの組み込み型であるスライス (Array 構造体) の内部オフセット (Array_array, Array_nel, Array_cap) と全体のサイズ (sizeof_Array) も typeinit 内で計算されるようになりました。これにより、スライスのメモリレイアウトがコンパイラ全体で一貫して定義されます。
  3. グローバル変数の昇格:

    • maxroundwidthptr は、以前は 6g/align.c 内の静的変数でしたが、src/cmd/gc/go.hEXTERN 宣言され、gc 全体でアクセス可能なグローバル変数となりました。これは、これらの値がコンパイラの共通部分で利用されるようになったことを示しています。
  4. Typedef 構造体の定義と利用:

    • src/cmd/gc/go.hTypedef 構造体が定義され、typedefs というグローバル配列が宣言されました。これは、Goの組み込み型エイリアス(int, uint, uintptr, float)の定義を構造化するために使用されます。
    • typeinit 関数内でこの typedefs 配列をループ処理し、各エイリアス型に対応する Type 構造体を初期化し、そのサイズを dowidth で計算しています。
  5. エラーチェックの追加:

    • gc/align.crnd および dowidth 関数に、maxround == 0widthptr == 0 のチェックが追加されました。これは、betypeinit が適切に呼び出され、これらの重要なアライメント関連のグローバル変数が初期化されていることを保証するためのものです。初期化されていない場合、fatal エラーでコンパイラが終了します。
  6. 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.cOFILES に追加され、コンパイル対象となりました。
  • src/cmd/gc/align.c: 新規作成されたファイルで、6g/align.c から移動されたアライメント計算ロジック(rnd, offmod, arrayelemwidth, widstruct, dowidth)が含まれています。また、typeinitbetypeinit という新しい初期化関数が追加されました。
  • src/cmd/gc/go.h:
    • Typedef 構造体と typedefs グローバル配列の宣言が追加されました。
    • Array_array, Array_nel, Array_cap, sizeof_Array といった配列のランタイム表現に関する EXTERN 宣言が追加されました。
    • maxroundwidthptr というアライメント関連のグローバル変数の EXTERN 宣言が追加されました。
    • mainlex, belexinit, besetptr の関数プロトタイプが削除され、typeinit, betypeinit のプロトタイプが追加されました。
  • src/cmd/gc/lex.c:
    • mainlex 関数が main にリネームされ、コンパイラのメインエントリポイントとなりました。
    • main 関数内で betypeinit()typeinit(LBASETYPE) が呼び出されるようになりました。
    • lexinit 関数から、以前 belexinitbesetptr で行われていた型初期化とアライメント関連の処理が削除されました。
  • 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 というマーカーを使用しています。この関数は、rndwidstruct などのヘルパー関数を呼び出して、正確なサイズとアライメントを決定します。

  • 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の場合)
    }
    

    この関数は、アーキテクチャに依存するがコンパイラ全体で共通して使用されるアライメント関連のグローバル変数 maxroundwidthptr を初期化します。これらは、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 関数内で、betypeinittypeinit が明示的に呼び出されるようになりました。これにより、コンパイル処理の早い段階で、すべての型サイズとアライメント情報が適切に設定されることが保証されます。

src/cmd/gc/go.h

  • グローバル変数の宣言:

    EXTERN  int maxround;
    EXTERN  int widthptr;
    

    maxroundwidthptrEXTERN 宣言されたことで、これらの変数が 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 の共通ヘッダーファイルに移動されました。これにより、スライスの内部構造に関する情報が中央集約され、コンパイラの異なる部分や、将来的に異なるバックエンドがこの共通の定義を利用できるようになります。

関連リンク

参考にした情報源リンク