[インデックス 17163] ファイルの概要
このコミットは、Go言語のARMアーキテクチャ向けアセンブラである cmd/5g
において、命令に関する情報を一元的に管理するためのリファクタリングを導入します。具体的には、各命令の特性(オペランドの読み書き、サイズ、命令の種類など)を定義する ProgInfo
構造体と、その情報を取得する proginfo
関数を導入し、命令のプロパティをハードコードされた switch
文からデータ駆動型のテーブルに移行します。これにより、コードの可読性、保守性、拡張性が向上します。
コミット
commit a07218385ce652f55547dc06e664eaab6a47be43
Author: Russ Cox <rsc@golang.org>
Date: Mon Aug 12 13:42:23 2013 -0400
cmd/5g: factor out prog information
Like CL 12637051, but for 5g instead of 6g.
R=ken2
CC=golang-dev
https://golang.org/cl/12779043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a07218385ce652f55547dc06e664eaab6a47be43
元コミット内容
cmd/5g: factor out prog information
(cmd/5g
: プログラム情報をファクタリングする)
Like CL 12637051, but for 5g instead of 6g.
(CL 12637051 と同様だが、6g の代わりに 5g 向け。)
変更の背景
Goコンパイラのバックエンド、特にアセンブラ(cmd/5g
はARM向け、cmd/6g
はAMD64向け)では、各機械語命令がどのような特性を持つか(例:どのレジスタを読み書きするか、メモリを操作するか、分岐命令かなど)を判断する必要がありました。これまでの実装では、これらの命令特性が peep.c
(ピーフホール最適化)や reg.c
(レジスタ割り当て)などのファイル内で、命令の種類ごとに個別の switch
文を使ってハードコードされていました。
このアプローチにはいくつかの問題がありました。
- 冗長性: 同じ命令特性のチェックが複数の場所で繰り返され、コードの重複が生じていました。
- 保守性の低さ: 新しい命令が追加されたり、既存の命令の特性が変更されたりした場合、関連するすべての
switch
文を更新する必要があり、エラーが発生しやすかった。 - 可読性の低下: 命令の特性がコード全体に散らばっているため、全体像を把握しにくく、コードの意図が不明瞭になることがありました。
このコミットは、cmd/6g
で先行して行われた同様のリファクタリング(CL 12637051)の cmd/5g
版として、これらの問題を解決することを目的としています。命令の特性をデータ駆動型のテーブルに集約することで、コードの重複を排除し、保守性と可読性を向上させます。
前提知識の解説
このコミットを理解するためには、以下の概念が役立ちます。
- Goコンパイラとアセンブラ (
cmd/5g
): Go言語のコンパイラは、Goのソースコードを機械語に変換する過程で、中間表現(IR)を生成し、最終的に各アーキテクチャ向けのアセンブラ(例:cmd/5g
はARM、cmd/6g
はAMD64)が機械語コードを生成します。cmd/5g
は、Goコンパイラの一部としてARMアーキテクチャ用の機械語命令を生成・最適化する役割を担います。 - 中間表現 (IR): コンパイラがソースコードを直接機械語に変換するのではなく、途中で用いる抽象的な表現です。Goコンパイラでは、
Prog
構造体がこの中間表現における個々の命令を表します。 - ピーフホール最適化 (Peephole Optimization): コンパイラ最適化の一種で、生成された機械語コードの小さな「窓」(ピーフホール)を覗き、非効率な命令シーケンスをより効率的なものに置き換える手法です。
peep.c
はこの最適化を担当します。 - レジスタ割り当て (Register Allocation): プログラムの実行速度を向上させるため、頻繁に使用される変数をCPUの高速なレジスタに割り当てるプロセスです。
reg.c
はこのレジスタ割り当てを担当します。 - データ駆動型プログラミング: ロジックをコード内にハードコードするのではなく、データ構造(テーブル、設定ファイルなど)によってロジックを制御するプログラミングパラダイムです。これにより、ロジックの変更が容易になり、コードの柔軟性と保守性が向上します。
Prog
構造体:src/cmd/5g/go.h
(またはsrc/cmd/5g/gg.h
を含むヘッダ) で定義されている、アセンブラの命令を表す主要な構造体です。各Prog
インスタンスは、オペコード (as
)、ソースオペランド (from
)、デスティネーションオペランド (to
) などの情報を含みます。CL
(Change List): Goプロジェクトにおけるコミットや変更セットを指す用語です。CL 12637051
は、AMD64アセンブラ (6g
) で同様の変更が行われた先行コミットを指します。
技術的詳細
このコミットの核心は、命令の特性を記述するための新しいデータ構造 ProgInfo
と、その情報を取得するための proginfo
関数、そして命令ごとの特性を定義する progtable
の導入です。
ProgInfo
構造体とフラグ
src/cmd/5g/opt.h
に新しく追加された ProgInfo
構造体は、uint32 flags;
という単一のフィールドを持ちます。この flags
フィールドは、以下のようなビットフラグの組み合わせで命令の特性を表します。
-
命令の種類:
Pseudo
: 擬似命令(例:TEXT
,GLOBL
,TYPE
など)。コード生成には直接関係しないが、コンパイラ内部で使われる。OK
: 命令に特筆すべき情報はないが、有効な命令である。Move
: 単純なデータ移動命令。Conv
: 型変換命令。Cjmp
: 条件付きジャンプ命令。Break
: 制御フローを中断する命令(フォールスルーしない)。例:RET
。Call
: 関数呼び出し命令。Jump
: 無条件ジャンプ命令。Skip
: データ命令(最適化時にスキップされるべき命令)。
-
オペランドのサイズ:
SizeB
,SizeW
,SizeL
,SizeQ
: バイト、ワード、ロング、クアッドワードのサイズ。SizeF
,SizeD
: 単精度浮動小数点数、倍精度浮動小数点数のサイズ。
-
オペランドのアクセス特性:
LeftAddr
: 左オペランドがアドレスとして扱われる。LeftRead
: 左オペランドが読み取られる。LeftWrite
: 左オペランドが書き込まれる。RegRead
: 中間レジスタが読み取られる(p->reg
フィールド)。CanRegRead
: 中間レジスタが読み取られる可能性がある。RightAddr
: 右オペランドがアドレスとして扱われる。RightRead
: 右オペランドが読み取られる。RightWrite
: 右オペランドが書き込まれる。
これらのフラグを組み合わせることで、各命令の動作を詳細かつ効率的に表現できます。
progtable
と proginfo
関数
src/cmd/5g/prog.c
に新しく追加された progtable
は、ALAST
(命令オペコードの最大値)までの配列で、各インデックスにその命令に対応する ProgInfo
構造体が格納されています。このテーブルは、コンパイラが生成する命令の基本的な情報を一元的に定義します。
proginfo(ProgInfo *info, Prog *p)
関数は、与えられた Prog
(p
) のオペコード (p->as
) を使用して progtable
から対応する ProgInfo
を取得し、info
ポインタが指す構造体にコピーします。また、この関数はいくつかの特殊なケース(例: RegRead
フラグの調整や、条件付き命令での RightRead
の追加)を処理し、最終的な ProgInfo
を返します。
peep.c
と reg.c
の変更
peep.c
と reg.c
では、これまで命令のオペコードに基づいて個別に記述されていた switch
文の多くが、proginfo
関数を呼び出して ProgInfo
フラグをチェックする形に置き換えられました。
例えば、peep.c
の peep
関数では、以前は ADATA
, AGLOBL
などの擬似命令をスキップするために明示的な case
文がありましたが、変更後は proginfo(&info, p); if(info.flags & Skip) continue;
のように、Skip
フラグをチェックするだけでよくなりました。
同様に、reg.c
の regopt
関数では、オペランドの読み書きを判断するための複雑な switch
文が、info.flags & LeftRead
, info.flags & RegRead
, info.flags & (RightAddr | RightRead | RightWrite)
といった簡潔なフラグチェックに置き換えられています。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の4つのファイルにわたります。
-
src/cmd/5g/opt.h
:ProgInfo
構造体の定義を追加。- 命令の特性を表すビットフラグの
enum
を追加。 proginfo
関数のプロトタイプ宣言を追加。
-
src/cmd/5g/peep.c
:peep
関数とsubprop
関数内で、命令の特性を判断するためにProgInfo
構造体とproginfo
関数を使用するように変更。- 命令の種類に応じた冗長な
switch
文を削除し、ProgInfo
フラグによるチェックに置き換え。
-
src/cmd/5g/prog.c
: (新規ファイル)progtable
という静的配列を定義し、各命令オペコードに対応するProgInfo
フラグを初期化。proginfo
関数の実装を提供。この関数はprogtable
を参照し、命令の特性フラグを返す。
-
src/cmd/5g/reg.c
:regopt
関数内で、命令の特性(特にオペランドの読み書き)を判断するためにProgInfo
構造体とproginfo
関数を使用するように変更。- 命令の種類に応じた複雑な
switch
文を削除し、ProgInfo
フラグによる簡潔なチェックに置き換え。
コアとなるコードの解説
src/cmd/5g/opt.h
の変更
// ... 既存のコード ...
typedef struct ProgInfo ProgInfo;
struct ProgInfo
{
uint32 flags; // the bits below
};
enum
{
// Pseudo-op, like TEXT, GLOBL, TYPE, PCDATA, FUNCDATA.
Pseudo = 1<<1,
// There's nothing to say about the instruction,
// but it's still okay to see.
OK = 1<<2,
// Size of right-side write, or right-side read if no write.
SizeB = 1<<3,
SizeW = 1<<4,
SizeL = 1<<5,
SizeQ = 1<<6,
SizeF = 1<<7, // float aka float32
SizeD = 1<<8, // double aka float64
// Left side: address taken, read, write.
LeftAddr = 1<<9,
LeftRead = 1<<10,
LeftWrite = 1<<11,
// Register in middle; never written.
RegRead = 1<<12,
CanRegRead = 1<<13,
// Right side: address taken, read, write.
RightAddr = 1<<14,
RightRead = 1<<15,
RightWrite = 1<<16,
// Instruction kinds
Move = 1<<17, // straight move
Conv = 1<<18, // size conversion
Cjmp = 1<<19, // conditional jump
Break = 1<<20, // breaks control flow (no fallthrough)
Call = 1<<21, // function call
Jump = 1<<22, // jump
Skip = 1<<23, // data instruction
};
void proginfo(ProgInfo*, Prog*);
この部分では、ProgInfo
構造体と、命令の様々な特性を表すビットフラグが定義されています。これらのフラグは、命令が擬似命令であるか (Pseudo
)、どのオペランドを読み書きするか (LeftRead
, RightWrite
など)、命令の種類 (Move
, Call
, Jump
など) を示します。proginfo
関数のプロトタイプもここで宣言されています。
src/cmd/5g/prog.c
の新規追加
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include <u.h>
#include <libc.h>
#include "gg.h"
#include "opt.h"
enum
{
RightRdwr = RightRead | RightWrite,
};
// This table gives the basic information about instruction
// generated by the compiler and processed in the optimizer.
// See opt.h for bit definitions.
//
// Instructions not generated need not be listed.
// As an exception to that rule, we typically write down all the
// size variants of an operation even if we just use a subset.
//
// The table is formatted for 8-space tabs.
static ProgInfo progtable[ALAST] = {
[ATYPE]= {Pseudo | Skip},
[ATEXT]= {Pseudo},
[AFUNCDATA]= {Pseudo},
[APCDATA]= {Pseudo},
// NOP is an internal no-op that also stands
// for USED and SET annotations, not the Intel opcode.
[ANOP]= {LeftRead | RightWrite},
// Integer.
[AADC]= {SizeL | LeftRead | RegRead | RightWrite},
// ... 多くの命令の定義が続く ...
// Jumps.
[AB]= {Jump},
[ABL]= {Call},
[ABEQ]= {Cjmp},
// ... 他の条件付きジャンプ命令 ...
[ARET]= {Break},
};
void
proginfo(ProgInfo *info, Prog *p)
{
*info = progtable[p->as];
if(info->flags == 0)
fatal("unknown instruction %P", p);
if((info->flags & RegRead) && p->reg == NREG) {
info->flags &= ~RegRead;
info->flags |= CanRegRead | RightRead;
}
if(((p->scond & C_SCOND) != C_SCOND_NONE) && (info->flags & RightWrite))
info->flags |= RightRead;
}
このファイルは、progtable
という静的配列を定義しています。このテーブルは、Goアセンブラの各命令オペコード (ATYPE
, ATEXT
, AADD
など) に対応する ProgInfo
フラグをマッピングします。これにより、命令の特性がコードからデータに分離されます。
proginfo
関数は、Prog
オブジェクトを受け取り、そのオペコードに基づいて progtable
から対応する ProgInfo
をルックアップします。また、特定の条件(例: RegRead
フラグと p->reg
が NREG
の場合、または条件付き命令で RightWrite
がある場合)に基づいてフラグを調整するロジックも含まれています。これにより、命令の動的な特性も考慮されます。
src/cmd/5g/peep.c
の変更例
// ... 既存のコード ...
peep(void)
{
Reg *r, *r1, *r2;
Prog *p;
int t;
ProgInfo info; // ProgInfo 構造体の宣言
/*
* complete R structure
*/
for(r=firstr; r!=R; r=r->link) {
r1 = r->link;
if(r1 == R)
break;
for(p = r->prog->link; p != r1->prog; p = p->link) {
proginfo(&info, p); // proginfo を呼び出して命令情報を取得
if(info.flags & Skip) // Skip フラグをチェック
continue;
r2 = rega();
r->link = r2;
r2->link = r1;
r2->prog = p;
p->regp = r2;
r2->p1 = r;
r->s1 = r2;
r2->s1 = r1;
r1->p1 = r2;
r = r2;
}
}
// ... 既存のコード ...
}
peep
関数では、以前は ADATA
, AGLOBL
などの擬似命令をスキップするために明示的な switch
文を使用していましたが、この変更により proginfo(&info, p); if(info.flags & Skip) continue;
のように、Skip
フラグをチェックするだけでよくなりました。これにより、コードがより簡潔になり、新しい擬似命令が追加された場合でも peep.c
を変更する必要がなくなります。
src/cmd/5g/reg.c
の変更例
// ... 既存のコード ...
regopt(Prog *firstp)
{
int i, z, nr;
uint32 vreg;
Bits bit;
ProgInfo info, info2; // ProgInfo 構造体の宣言
// ... 既存のコード ...
/*
* build reg graph
*/
nr = 0;
for(p=firstp; p != P; p = p->link) {
proginfo(&info, p); // proginfo を呼び出して命令情報を取得
if(info.flags & Skip) // Skip フラグをチェック
continue;
// ... 既存のコード ...
}
// ... 既存のコード ...
if(r1 != R) {
proginfo(&info2, r1->prog); // proginfo を呼び出して命令情報を取得
if(info2.flags & Break) { // Break フラグをチェック
r->p1 = R;
r1->s1 = R;
}
}
// ... 既存のコード ...
if(info.flags & LeftRead) { // LeftRead フラグをチェック
bit = mkvar(r, &p->from);
for(z=0; z<BITS; z++)
r->use1.b[z] |= bit.b[z];
}
if(info.flags & RegRead) { // RegRead フラグをチェック
if(p->from.type != D_FREG)
r->use1.b[0] |= RtoB(p->reg);
else
r->use1.b[0] |= FtoB(p->reg);
}
if(info.flags & (RightAddr | RightRead | RightWrite)) { // 右オペランドのアクセス特性をチェック
bit = mkvar(r, &p->to);
if(info.flags & RightAddr)
setaddrs(bit);
if(info.flags & RightRead)
for(z=0; z<BITS; z++)
r->use2.b[z] |= bit.b[z];
if(info.flags & RightWrite)
for(z=0; z<BITS; z++)
r->set.b[z] |= bit.b[z];
}
// ... 既存のコード ...
}
regopt
関数でも、命令の特性を判断するための多くの switch
文が proginfo
関数と ProgInfo
フラグのチェックに置き換えられています。これにより、コードが大幅に簡素化され、命令の特性に関するロジックが一元化されました。特に、オペランドの読み書きに関する複雑な条件分岐が、LeftRead
, RegRead
, RightAddr
, RightRead
, RightWrite
といった明確なフラグの組み合わせで表現できるようになりました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のコンパイラに関するドキュメント: Go言語のコンパイラの内部構造に関する公式ドキュメントは、Goのソースコードリポジトリ内の
src/cmd/compile/internal/
ディレクトリや、Goのブログ記事などで見つけることができます。 - CL 12637051 (6g版の先行コミット): https://golang.org/cl/12637051 (このコミットの元となった
cmd/6g
向けのリファクタリング)
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/5g
ディレクトリ) - Go言語のコミット履歴と変更リスト (Change List)
- コンパイラ最適化に関する一般的な知識 (ピーフホール最適化、レジスタ割り当てなど)
- データ駆動型プログラミングの概念