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

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

このコミットは、Goコンパイラのコード生成ロジックにおける重要なリファクタリングを目的としています。具体的には、アーキテクチャに依存しないポータブルなコード生成(基本的なステートメントの処理)に関するコードを、特定のアーキテクチャ(この場合はx86-64向けの6g)のコンパイラから、より汎用的なGoコンパイラ(gc)のコア部分へと移動させています。これにより、Goコンパイラのモジュール性と移植性が向上し、将来的な他アーキテクチャへの対応が容易になります。

コミット

commit bac922c6e15f6c6b7378178087e0772c7aa0745a
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 30 19:15:07 2009 -0700

    move portable code generation (basic statements) to gc.
    
    R=ken
    OCL=26929
    CL=26929

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

https://github.com/golang/go/commit/bac922c6e15f6c6b7378178087e0772c7aa0745a

元コミット内容

move portable code generation (basic statements) to gc.

R=ken
OCL=26929
CL=26929

変更の背景

Goコンパイラは、初期の段階から複数のアーキテクチャをサポートするように設計されていました。しかし、初期の実装では、コード生成の一部が各アーキテクチャ固有のコンパイラ(例: 6gはx86-64、8gはx86、5gはARM)に分散していました。これにより、共通のコード生成ロジックが重複したり、新しいアーキテクチャを追加する際に多くのコードを複製・修正する必要が生じたりする問題がありました。

このコミットの背景には、Goコンパイラの構造をよりモジュール化し、アーキテクチャ固有の部分と共通の部分を明確に分離するという設計思想があります。ポータブルな(移植性のある)コード生成ロジックを汎用コンパイラ(gc)に移動させることで、以下の利点が得られます。

  1. コードの再利用性向上: 基本的なステートメント(if, for, switchなど)のコード生成ロジックは、どのアーキテクチャでも共通して利用できるため、一元化することでコードの重複を排除し、保守性を高めます。
  2. 移植性の向上: 新しいアーキテクチャをサポートする際に、アーキテクチャ固有のバックエンドのみを実装すればよくなり、開発コストと複雑さを削減できます。
  3. コンパイラ構造の明確化: コンパイラの各フェーズ(フロントエンド、中間表現、バックエンド)の役割がより明確になり、コードベースの理解と改善が容易になります。

この変更は、Goコンパイラが成熟し、より多くのプラットフォームへの対応を目指す上で不可欠なステップでした。

前提知識の解説

このコミットを理解するためには、Goコンパイラの基本的なアーキテクチャとコード生成の概念について知っておく必要があります。

Goコンパイラのアーキテクチャ (初期のgc/6g/8g/5g)

初期のGoコンパイラは、gc(Go Compiler)という名前で知られていますが、実際には複数のコンポーネントから構成されていました。

  • フロントエンド: ソースコードの字句解析、構文解析、型チェック、抽象構文木(AST)の構築、および中間表現(IR)への変換を担当します。この部分はアーキテクチャに依存しません。
  • バックエンド: 中間表現を受け取り、特定のターゲットアーキテクチャ向けの機械語コードを生成します。初期のGoコンパイラでは、このバックエンドがアーキテクチャごとに分かれていました。
    • 6g: x86-64 (64-bit Intel/AMD) アーキテクチャ向けのコンパイラ。
    • 8g: x86 (32-bit Intel/AMD) アーキテクチャ向けのコンパイラ。
    • 5g: ARM アーキテクチャ向けのコンパイラ。

これらのコンパイラは、それぞれが独立した実行ファイルとして存在し、Go言語のソースコードを対応するアーキテクチャのオブジェクトファイルにコンパイルしていました。このコミット以前は、6gなどのアーキテクチャ固有のコンパイラが、ASTから機械語への変換の大部分を担っていました。

コード生成のフェーズ

コード生成は、コンパイラの最終段階であり、中間表現をターゲットマシンが実行できる命令に変換するプロセスです。一般的に、コード生成は以下のステップを含みます。

  1. 中間表現の最適化: ターゲットマシンに依存しない最適化(例: 定数畳み込み、デッドコード削除)が行われます。
  2. 命令選択: 中間表現の操作を、ターゲットアーキテクチャの具体的な命令にマッピングします。
  3. レジスタ割り当て: 変数や中間結果をCPUのレジスタに割り当てます。レジスタは高速ですが数が限られているため、効率的な割り当てが重要です。
  4. 命令スケジューリング: 命令の実行順序を最適化し、パイプラインのストールを減らします。
  5. コード出力: 最終的な機械語コードを生成し、オブジェクトファイルとして出力します。

ポータブルなコード生成

「ポータブルなコード生成」とは、特定のCPUアーキテクチャやオペレーティングシステムに依存しない、汎用的なコード生成ロジックを指します。例えば、if文やforループのような制御フロー、変数宣言、基本的な代入操作などは、どのアーキテクチャでも概念的には同じように処理できます。これらのロジックをアーキテクチャ固有のコンポーネントから分離し、共通の基盤に置くことで、コンパイラの設計がよりクリーンになり、将来的な拡張が容易になります。

このコミットでは、特に「basic statements」(基本的なステートメント)のコード生成がポータブルな部分として認識され、gcへと移動されました。これにより、6gなどのアーキテクチャ固有のコンパイラは、より低レベルな、アーキテクチャに特化した最適化や命令選択に集中できるようになります。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのコードベースにおける機能の責任分担を再定義した点にあります。具体的には、src/cmd/6g/gen.c に存在していた、Go言語の基本的なステートメント(ifforswitchgotobreakcontinue、変数宣言、代入など)のコード生成ロジックが、src/cmd/gc/gen.c という新しいファイルに移動されました。

ファイルの移動と再編成

  • src/cmd/6g/gen.c からの削除:

    • gen 関数: ASTノードを受け取り、対応する機械語命令を生成する主要な関数。
    • allocparams 関数: 関数の引数とローカル変数のスタック上のオフセットを計算する関数。
    • newlab, checklabels, findlab 関数: ラベル(goto文のターゲットなど)の管理に関連する関数。
    • cgen_dcl, cgen_as, cgen_callmeth, cgen_proc 関数: 宣言、代入、メソッド呼び出し、ゴルーチン/defer呼び出しのコード生成に関連する関数。
    • argsize 関数: 関数の引数と戻り値の合計サイズを計算する関数。
    • clearfat 関数: 構造体などの「fat object」をゼロクリアする関数。 これらの関数は、6ggen.cから完全に削除されました。
  • src/cmd/gc/gen.c の新規作成と追加:

    • 上記で6g/gen.cから削除されたすべての関数が、src/cmd/gc/gen.c にそのまま、またはわずかな修正を加えて追加されました。これにより、これらの関数がアーキテクチャ非依存のコード生成ロジックとして一元管理されることになります。
    • この新しい gen.c ファイルは、Goコンパイラの汎用部分としてコンパイルされるよう、src/cmd/gc/Makefilegen.$O が追加されました。

ヘッダーファイルの変更

  • src/cmd/6g/gg.h からの削除:

    • Prog, Label 構造体の定義、および continpc, breakpc, pc, firstpc, labellist といったグローバル変数の宣言が削除されました。これらはコード生成の状態を管理するためのものであり、ポータブルなコード生成ロジックと共に移動されました。
    • 削除されたコード生成関数のプロトタイプも削除されました。
  • src/cmd/gc/go.h への追加:

    • Prog, Label 構造体の定義、および関連するグローバル変数の宣言が src/cmd/gc/go.h に移動されました。
    • 移動されたコード生成関数のプロトタイプが追加されました。これにより、gcの他のコンポーネントがこれらのポータブルなコード生成関数を呼び出せるようになります。

その他の変更

  • src/cmd/6g/align.c から symstringo = lookup(".stringo"); の行が削除され、src/cmd/gc/lex.c に移動されました。これは文字列リテラルのシンボルルックアップに関する初期化処理であり、コンパイラのフロントエンドに近い部分に移動されたと考えられます。
  • src/cmd/6g/gsubr.cgusedgjmp という新しいヘルパー関数が追加されました。これらは、コード生成の過程で必要となる汎用的なジャンプ命令や、変数が使用されたことをマークするためのユーティリティ関数です。

影響と意義

この変更により、Goコンパイラのコード生成パイプラインは以下のように変化しました。

  1. ASTから中間表現への変換: フロントエンド(gcのパーサーと型チェッカー)がASTを構築し、それをより抽象的な中間表現(Goコンパイラでは「Node」と呼ばれる構造体で表現されることが多い)に変換します。
  2. ポータブルなコード生成: src/cmd/gc/gen.c に移動された関数群が、この中間表現を受け取り、基本的なステートメントや制御フローを、まだ特定の機械語に特化していない、より低レベルな命令シーケンス(Prog構造体で表現される)に変換します。この段階では、まだレジスタ割り当てやアーキテクチャ固有の最適化は行われません。
  3. アーキテクチャ固有のコード生成: 6gなどのアーキテクチャ固有のコンパイラは、この低レベルな命令シーケンスを受け取り、ターゲットアーキテクチャのレジスタ割り当て、命令選択、最終的な機械語コード生成を行います。

この分離により、Goコンパイラはよりクリーンなアーキテクチャを持ち、各コンポーネントが明確な責任を持つようになりました。これは、Go言語が成長し、多様なプラットフォームで利用されるための基盤を強化する上で非常に重要なステップでした。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  • src/cmd/6g/gen.c:

    • gen, allocparams, newlab, checklabels, findlab, cgen_dcl, cgen_as, cgen_callmeth, cgen_proc, argsize, clearfat など、多数のコード生成関連関数が削除されました。これは、これらの機能が6gからgcへ移動したことを示します。
    • 変更行数: 592行削除, 3行追加。
  • src/cmd/6g/gg.h:

    • Prog, Label 構造体の定義、および関連するグローバル変数 (continpc, breakpc, pc, firstpc, labellist) の宣言が削除されました。
    • 削除されたコード生成関数のプロトタイプも削除されました。
    • 変更行数: 25行削除。
  • src/cmd/gc/Makefile:

    • gen.$OOFILES に追加されました。これは、新しく作成された src/cmd/gc/gen.cgcコンパイラの一部としてコンパイルされることを意味します。
    • 変更行数: 1行追加。
  • src/cmd/gc/align.c:

    • argsize 関数が src/cmd/6g/gen.c から移動されてきました。
    • 変更行数: 32行追加。
  • src/cmd/gc/gen.c:

    • 新規ファイルとして追加されました。
    • src/cmd/6g/gen.c から削除されたすべてのコード生成関連関数(gen, allocparams, newlab, checklabels, findlab, cgen_dcl, cgen_as, cgen_callmeth, cgen_proc, clearfat など)がここに移動されました。
    • 変更行数: 505行追加。
  • src/cmd/gc/go.h:

    • Prog, Label 構造体の定義、および関連するグローバル変数 (continpc, breakpc, pc, firstpc, labellist) の宣言が src/cmd/6g/gg.h から移動されてきました。
    • 移動されたコード生成関数のプロトタイプが追加されました。
    • 変更行数: 66行追加, 14行削除。
  • src/cmd/gc/lex.c:

    • symstringo = lookup(".stringo"); の行が src/cmd/6g/align.c から移動されてきました。
    • 変更行数: 1行追加, 1行削除。

コアとなるコードの解説

移動された主要な関数とその役割について解説します。これらの関数は、Go言語のソースコードから中間表現を経て、最終的な機械語命令に近い形式を生成する上で中心的な役割を担っています。

gen(Node *n)

この関数は、Go言語のステートメント(ifforswitchreturn、代入など)に対応する中間表現(Node)を受け取り、それらを低レベルの命令シーケンスに変換する主要なエントリポイントです。 例えば、if文の場合、条件式の評価、真偽に応じたジャンプ命令の生成、そして各ブロックのコード生成を調整します。forループの場合も、初期化、条件、インクリメント、ループ本体のコード生成と、ループの継続・終了のためのジャンプ命令を生成します。 この関数が6gからgcに移動したことは、Go言語の基本的な制御フローのコード生成がアーキテクチャに依存しないことを明確に示しています。

allocparams(void)

この関数は、関数の引数とローカル変数(自動変数)がスタック上で占める領域を計算し、それぞれの変数にスタック上のオフセットを割り当てます。Goの関数呼び出し規約に従って、引数と戻り値、ローカル変数がどのようにメモリに配置されるかを決定します。 スタックフレームのレイアウトは、基本的な部分ではアーキテクチャに依存しないため、この関数もポータブルなコード生成の一部としてgcに移動されました。

newlab(int op, Sym *s), checklabels(void), findlab(Sym *s)

これらの関数は、goto文やbreak/continue文で使用されるラベルの管理を担当します。

  • newlab: 新しいラベルを定義し、現在のコード生成位置(pc)に関連付けます。
  • checklabels: すべてのラベルが正しく定義され、参照されているかをチェックし、未定義のラベルや再定義されたラベルがないかを確認します。また、goto文のジャンプ先を解決(パッチ適用)します。
  • findlab: 特定のシンボルに対応するラベルを検索します。 ラベルの概念とそれらの解決は、プログラミング言語の制御フローにおいて普遍的なものであり、特定のCPUアーキテクチャに依存しないため、これらの関数もgcに移動されました。

cgen_dcl(Node *n)

この関数は、変数の宣言(varキーワードなど)のコード生成を扱います。特に、ヒープに割り当てられる変数(エスケープ解析によってヒープに配置されると判断された変数)の場合、そのメモリ割り当てと初期化のコードを生成します。スタックに割り当てられる変数の場合は、通常、特別なコード生成は不要です。 変数の宣言とメモリ割り当ての基本的なロジックは、アーキテクチャに依存しないため、gcに移動されました。

cgen_as(Node *nl, Node *nr)

この関数は、代入操作(nl = nr)のコード生成を担当します。nlは左辺(代入先)、nrは右辺(代入元)を表します。 nrN(nil)の場合、nlをゼロクリアするコードを生成します。特に、構造体などの大きなオブジェクト(isfat(tl)で判定される「fat object」)のゼロクリアには、効率的なメモリ操作(REP STOSQREP STOSBのような命令)が使用されます。 基本的な代入操作やメモリのゼロクリアは、アーキテクチャに依存しないため、この関数もgcに移動されました。

cgen_callmeth(Node *n, int proc), cgen_proc(Node *n, int proc)

これらの関数は、メソッド呼び出し、ゴルーチン(goキーワード)、およびdefer呼び出しのコード生成を扱います。

  • cgen_callmeth: インターフェースではないメソッド呼び出しを処理します。Goのメソッド呼び出しは、レシーバを最初の引数として通常の関数呼び出しに変換されるため、その変換ロジックを生成します。
  • cgen_proc: goキーワードによる新しいゴルーチンの起動、またはdeferキーワードによる遅延実行関数の呼び出しのコードを生成します。これには、ランタイム関数(newproc, deferprocなど)の呼び出しが含まれます。 これらの呼び出しのセマンティクスはGo言語のランタイムに依存しますが、そのコード生成の基本的なパターンはアーキテクチャに依存しないため、gcに移動されました。

clearfat(Node *nl)

この関数は、構造体や配列などの「fat object」(複数のバイトを占めるオブジェクト)をゼロで埋めるコードを生成します。これは、変数の初期化や代入の際に、メモリ領域を確実にクリアするために使用されます。 メモリのゼロクリアは、どのアーキテクチャでも共通して必要となる操作であり、そのための効率的な命令シーケンス(例: REP STOSQ)の生成ロジックはポータブルです。

これらの関数が6gからgcに移動されたことで、Goコンパイラのコード生成バックエンドは、アーキテクチャ固有の最適化や命令選択に特化し、より高レベルなステートメントの処理は汎用的なgcが担当するという、明確な役割分担が実現されました。

関連リンク

参考にした情報源リンク

  • Goコンパイラの設計に関する初期の議論やドキュメント(Goの公式リポジトリ内のdoc/go1.htmldoc/go_spec.html、またはGoのメーリングリストアーカイブなど)
  • Goコンパイラのソースコード(特にsrc/cmd/compile/internal/gcおよびsrc/cmd/compile/internal/<arch>ディレクトリ)
  • コンパイラ設計に関する一般的な知識(中間表現、コード生成、最適化など)