[インデックス 13941] ファイルの概要
このコミットは、Goコンパイラのコード生成フェーズにおける最適化に関するものです。具体的には、cmd/6g
(32-bit x86アーキテクチャ向けコンパイラ) と cmd/8g
(64-bit x86アーキテクチャ向けコンパイラ) において、中間表現 (IR) のノードタイプである OINDREG
、ODOT
、ODOTPTR
のハンドリングを igen
関数に追加することで、生成されるアセンブリコードの効率を向上させています。これにより、特に LEAL
(Load Effective Address) / LEAQ
命令の削減と、8g
コンパイラにおけるレジスタ割り当ての改善が図られています。
コミット
commit f4e76d5e0222c40e9b98ba19e6628e49f14ecc12
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Sep 24 23:07:44 2012 +0200
cmd/6g, cmd/8g: add OINDREG, ODOT, ODOTPTR cases to igen.
Apart from reducing the number of LEAL/LEAQ instructions by about
30%, it gives 8g easier registerization in several cases,
for example in strconv. Performance with 6g is not affected.
Before (386):
src/pkg/strconv/decimal.go:22 TEXT (*decimal).String+0(SB),$240-12
src/pkg/strconv/extfloat.go:540 TEXT (*extFloat).ShortestDecimal+0(SB),$584-20
After (386):
src/pkg/strconv/decimal.go:22 TEXT (*decimal).String+0(SB),$196-12
src/pkg/strconv/extfloat.go:540 TEXT (*extFloat).ShortestDecimal+0(SB),$420-20
Benchmarks with GOARCH=386 (on a Core 2).
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 7110191000 7079644000 -0.43%
BenchmarkFannkuch11 7769274000 7766514000 -0.04%
BenchmarkGobDecode 33454820 34755400 +3.89%
BenchmarkGobEncode 11675710 11007050 -5.73%
BenchmarkGzip 2013519000 1593855000 -20.84%
BenchmarkGunzip 253368200 242667600 -4.22%
BenchmarkJSONEncode 152443900 120763400 -20.78%
BenchmarkJSONDecode 304112800 247461800 -18.63%
BenchmarkMandelbrot200 29245520 29240490 -0.02%
BenchmarkParse 8484105 8088660 -4.66%
BenchmarkRevcomp 2695688000 2841263000 +5.40%
BenchmarkTemplate 363759800 277271200 -23.78%
benchmark old ns/op new ns/op delta
BenchmarkAtof64Decimal 127 129 +1.57%
BenchmarkAtof64Float 166 164 -1.20%
BenchmarkAtof64FloatExp 308 300 -2.60%
BenchmarkAtof64Big 584 571 -2.23%
BenchmarkAppendFloatDecimal 440 430 -2.27%
BenchmarkAppendFloat 995 776 -22.01%
BenchmarkAppendFloatExp 897 746 -16.83%
BenchmarkAppendFloatNegExp 900 752 -16.44%
BenchmarkAppendFloatBig 1528 1228 -19.63%
BenchmarkAppendFloat32Integer 443 453 +2.26%
BenchmarkAppendFloat32ExactFraction 812 661 -18.60%
BenchmarkAppendFloat32Point 1002 773 -22.85%
BenchmarkAppendFloat32Exp 858 725 -15.50%
BenchmarkAppendFloat32NegExp 848 728 -14.15%
BenchmarkAppendFloat64Fixed1 447 431 -3.58%
BenchmarkAppendFloat64Fixed2 480 462 -3.75%
BenchmarkAppendFloat64Fixed3 461 457 -0.87%
BenchmarkAppendFloat64Fixed4 509 484 -4.91%
Update #1914.
R=rsc, nigeltao
CC=golang-dev, remy
https://golang.org/cl/6494107
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f4e76d5e0222c40e9b98ba19e6628e49f14ecc12
元コミット内容
cmd/6g, cmd/8g: add OINDREG, ODOT, ODOTPTR cases to igen.
このコミットは、Goコンパイラの 6g
(32-bit x86) および 8g
(64-bit x86) バックエンドにおいて、igen
関数に OINDREG
、ODOT
、ODOTPTR
という中間表現ノードの処理ケースを追加するものです。これにより、LEAL
/LEAQ
命令の生成を約30%削減し、特に 8g
コンパイラでのレジスタ割り当て(registerization)を容易にすることを目的としています。strconv
パッケージのベンチマークで顕著な改善が見られ、6g
のパフォーマンスには影響がないとされています。
変更の背景
Goコンパイラは、ソースコードを中間表現 (IR) に変換し、その後、ターゲットアーキテクチャ向けのアセンブリコードを生成します。このプロセスにおいて、効率的なアセンブリコードを生成することは、プログラムの実行速度とバイナリサイズに直結します。
以前のGoコンパイラでは、構造体のフィールドアクセスやポインタのデリファレンスといった操作が、アセンブリコードレベルで最適に表現されていないケースがありました。特に、LEAL
(Load Effective Address) や LEAQ
(Load Effective Address Quadword) 命令は、メモリのアドレス計算を行う際に使用されますが、これが過剰に生成されると、コードの冗長性や実行効率の低下を招く可能性があります。
このコミットの背景には、Goコンパイラが生成するアセンブリコードの品質を向上させ、特に strconv
のような数値変換処理において、より効率的なレジスタ利用と命令削減を実現したいという意図があります。これにより、Goプログラム全体のパフォーマンス向上とバイナリサイズの削減が期待されます。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が必要です。
- Goコンパイラ (gc): Go言語の公式コンパイラは、
gc
(Go Compiler) と呼ばれるツールチェーンの一部です。歴史的に、各アーキテクチャ向けのコンパイラは6g
(amd64),8g
(386),5g
(arm) のように命名されていました。このコミットが対象としているのは、6g
(amd64) と8g
(386) です。 - 中間表現 (IR): コンパイラがソースコードを解析した後、最終的な機械語に変換する前に内部的に使用する抽象的な表現です。Goコンパイラでは、AST (Abstract Syntax Tree) のような構造がIRとして利用されます。
igen
関数: Goコンパイラのコード生成フェーズにおいて、中間表現のノードを処理し、アセンブリコードを生成するための重要な関数の一つです。igen
は "generate immediate" の略で、ノードが表す値をレジスタまたはメモリに配置するための処理を行います。ONAME
: 中間表現におけるノードタイプの一つで、変数や関数名などのシンボルを表します。OINDREG
: 中間表現におけるノードタイプの一つで、レジスタを介した間接参照(例:*reg
)を表します。ODOT
: 中間表現におけるノードタイプの一つで、構造体のフィールドアクセス(例:s.field
)を表します。ODOTPTR
: 中間表現におけるノードタイプの一つで、ポインタを介した構造体のフィールドアクセス(例:p->field
または Go のp.field
)を表します。LEAL
(Load Effective Address) /LEAQ
(Load Effective Address Quadword): x86/x64アーキテクチャのアセンブリ命令です。通常、メモリのアドレスを計算し、その結果をレジスタに格納するために使用されます。例えば、LEAL (%eax, %ebx, 4), %ecx
は%ecx = %eax + %ebx * 4
を計算します。この命令は、実際にはメモリにアクセスせず、アドレス計算のみを行うため、算術演算としても利用されることがあります。しかし、単純なレジスタ間接参照で済む場合にLEAL
/LEAQ
が使われると、命令の冗長性やパイプラインの効率低下を招く可能性があります。- レジスタ割り当て (Registerization): コンパイラの最適化の一つで、頻繁にアクセスされる変数をメモリではなくCPUのレジスタに割り当てることで、データアクセスを高速化する技術です。レジスタはメモリよりもはるかに高速にアクセスできます。
strconv
パッケージ: Go標準ライブラリの一つで、文字列と数値の相互変換(例:int
からstring
、float
からstring
)を行う機能を提供します。数値変換は計算負荷が高く、コンパイラのコード生成効率がパフォーマンスに大きく影響します。regalloc
/regfree
: Goコンパイラのレジスタ割り当てに関連する関数で、レジスタの確保と解放を行います。gmove
: Goコンパイラのコード生成関数の一つで、あるノードから別のノードへ値を移動させるアセンブリ命令を生成します。ATESTB
: x86アセンブリのTEST
命令に相当するGoコンパイラの内部命令で、バイト単位のビットテストを行います。ここでは、ポインタがnil
でないことを確認するために使用されています。nodintconst
: 整数定数ノードを作成するGoコンパイラの内部関数です。
技術的詳細
このコミットの核心は、Goコンパイラの igen
関数が、OINDREG
、ODOT
、ODOTPTR
といった中間表現ノードをより直接的かつ効率的に処理するように拡張された点にあります。
igen
関数の役割
igen
関数は、与えられた中間表現ノード n
が表す値を、ターゲットノード a
に「即値」として(つまり、レジスタやメモリ参照として)設定することを試みます。これまでの実装では、これらのノードタイプが igen
で直接処理されず、より汎用的なコード生成パスにフォールバックしていた可能性があります。その結果、例えば構造体フィールドへのアクセス (ODOT
, ODOTPTR
) が、まずアドレスを計算し、そのアドレスをレジスタにロードし、さらにそのレジスタをデリファレンスするといった、複数のアセンブリ命令を必要とする非効率なコードを生成していました。特に LEAL
/LEAQ
命令が、本来不要なアドレス計算のために頻繁に生成される原因となっていました。
OINDREG
の追加
OINDREG
は、既にレジスタに格納されている値への間接参照を表します。igen
にこのケースを追加することで、コンパイラは、レジスタに格納されたポインタをデリファレンスする際に、余分なアドレス計算命令を生成することなく、直接そのレジスタを間接参照として利用できるようになります。これにより、LEAL
/LEAQ
命令の削減に貢献します。
コードでは、n->val.u.reg != D_SP
の条件でレジスタの参照カウントをインクリメントしています。これは、igen
の呼び出し元が regfree
を呼び出してレジスタを解放する必要があることを示しています。D_SP
(スタックポインタ) は特殊なレジスタであり、参照カウントの管理から除外されています。
ODOT
の追加
ODOT
は、構造体のフィールドアクセスを表します。例えば s.field
のような場合です。igen
に ODOT
のケースを追加することで、コンパイラは、構造体 s
のベースアドレスにフィールド field
のオフセットを直接加算したアドレスを、効率的に表現できるようになります。これにより、LEAL
/LEAQ
を使わずに、より直接的なメモリアクセス命令を生成することが可能になります。
実装では、まず n->left
(構造体 s
を表すノード) を igen
で処理し、その結果を a
に格納します。次に、a->xoffset
に n->xoffset
(フィールドのオフセット) を加算し、a->type
を n->type
(フィールドの型) に設定します。これにより、a
は構造体フィールドへの直接的な参照を表すようになります。
ODOTPTR
の追加
ODOTPTR
は、ポインタを介した構造体のフィールドアクセスを表します。例えば p.field
(Goでは (*p).field
と同じ) のような場合です。このケースは ODOT
よりも複雑で、ポインタのデリファレンスとフィールドオフセットの加算の両方を考慮する必要があります。
igen
に ODOTPTR
のケースを追加することで、コンパイラは以下の最適化を行います。
regalloc(a, types[tptr], res)
: 結果を格納するためのレジスタa
を確保し、ポインタ型 (tptr
) を割り当てます。cgen(n->left, a)
: ポインタp
を表すn->left
をcgen
で処理し、その値をレジスタa
に格納します。if(n->xoffset != 0)
: フィールドオフセットが0でない場合(つまり、構造体の先頭フィールドでない場合)に、追加のチェックを行います。if(n->left->type->type->width >= unmappedzero)
: ポインタが指す構造体のサイズがunmappedzero
(通常はページサイズ) 以上の場合、ポインタがnil
でないことを明示的にチェックします。これは、大きな構造体の場合に、nil
ポインタをデリファレンスしようとした際に、アクセス違反が発生する前に検出するための安全策です。ATESTB
命令 (x86のTEST
命令に相当) を使用して、ポインタが指すアドレスの0オフセットにあるバイトをテストします。a->op = OINDREG
:a
のオペレーションをOINDREG
に設定します。これにより、a
はレジスタに格納されたポインタの間接参照を表すようになります。a->xoffset += n->xoffset
:a
のオフセットにフィールドのオフセットを加算します。a->type = n->type
:a
の型をフィールドの型に設定します。
この一連の処理により、ODOTPTR
も LEAL
/LEAQ
を介さずに、より直接的なレジスタ間接参照とオフセット加算の組み合わせで表現できるようになります。
src/cmd/8g/gsubr.c
の変更
ginit
、gclean
、anyregalloc
関数におけるレジスタのループ範囲が D_AL
から D_AX
に変更されています。これは、レジスタの定義が変更されたか、またはレジスタ管理のロジックがより正確になったことを示唆しています。D_AL
は AX
レジスタの下位8ビットを表すため、D_AX
に変更することで、より広範なレジスタを対象とするようになった可能性があります。
src/cmd/gc/go.h
の変更
go.h
はGoコンパイラの共通ヘッダファイルで、中間表現のノード構造体 Node
や型構造体 Type
の定義が含まれています。このコミットでは、Type
構造体のコメントが更新され、type
フィールドと width
フィールドの役割がより明確に記述されています。
type
:TFIELD
の場合は実際の型、TARRAY
,TCHAN
,TMAP
,TPTRxx
の場合は要素型。width
:TFIELD
の場合はオフセット、それ以外の場合は幅。down
:TFIELD
の場合は次の構造体フィールド、TMAP
の場合はキー型。
これらのコメントの更新は、コードの可読性と保守性を向上させるためのものです。
パフォーマンスへの影響
コミットメッセージに記載されているベンチマーク結果は、この最適化が特に GOARCH=386
(32-bit x86) 環境で顕著な効果をもたらしたことを示しています。
LEAL
/LEAQ
命令の約30%削減: これは、コンパイラが生成するアセンブリコードの命令数を直接的に減らし、バイナリサイズの削減と実行効率の向上に寄与します。8g
(amd64) のレジスタ割り当て改善:igen
がこれらのノードを直接処理することで、コンパイラはより多くの値をレジスタに保持できるようになり、メモリへのアクセス回数を減らすことができます。これは特にstrconv
のような数値演算が頻繁に行われるコードで効果的です。6g
(386) のパフォーマンスは影響なし: これは、6g
のコード生成パスがこれらのノードタイプを既に効率的に処理していたか、または386
アーキテクチャの特性上、この種の最適化がパフォーマンスに与える影響が小さかったことを示唆しています。- ベンチマーク結果:
Gzip
,JSONEncode
,JSONDecode
,Template
など、I/Oやデータ処理に関連するベンチマークで20%以上の大幅な改善が見られます。これは、これらの処理が文字列操作や構造体アクセスを頻繁に行うため、今回の最適化の恩恵を大きく受けたことを示しています。strconv
関連のベンチマーク (BenchmarkAppendFloat
など) でも、同様に20%前後の改善が見られます。これは、strconv
が数値と文字列の変換において、構造体やポインタの操作を多用するため、レジスタ割り当ての改善が直接的に効いた結果と考えられます。- 一部のベンチマーク (
BenchmarkGobDecode
,BenchmarkRevcomp
,BenchmarkAtof64Decimal
,BenchmarkAppendFloat32Integer
) ではわずかな回帰が見られますが、全体としては大幅なパフォーマンス向上が達成されています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルに集中しています。
-
src/cmd/6g/cgen.c
:igen
関数にOINDREG
,ODOT
,ODOTPTR
のswitch
ケースが追加されました。OINDREG
では、レジスタの参照カウントをインクリメントし、ノードをそのまま返します。ODOT
では、左の子ノードをigen
で処理し、オフセットを加算します。ODOTPTR
では、レジスタを確保し、左の子ノードをcgen
で処理した後、必要に応じてnil
チェックを行い、オフセットを加算してOINDREG
に変換します。
-
src/cmd/8g/cgen.c
:src/cmd/6g/cgen.c
と同様に、igen
関数にOINDREG
,ODOT
,ODOTPTR
のswitch
ケースが追加されました。実装は6g
のものとほぼ同じです。cgen
関数内のisslice(nl->type)
ブロックで、n1.op = OINDREG;
が削除され、n1.xoffset = Array_cap;
がn1.xoffset += Array_cap;
に変更されています。これは、スライス容量のアクセス方法がより効率的になったことを示唆しています。
-
src/cmd/8g/gsubr.c
:ginit
,gclean
,anyregalloc
関数内のレジスタループの開始インデックスがD_AL
からD_AX
に変更されました。
-
src/cmd/gc/go.h
:Type
構造体のtype
,width
,down
フィールドに関するコメントが更新され、その役割がより詳細に記述されました。
コアとなるコードの解説
src/cmd/6g/cgen.c
および src/cmd/8g/cgen.c
の igen
関数
// 変更前 (例: 6g)
// switch(n->op) {
// case ONAME:
// // ...
// }
// 変更後 (例: 6g)
switch(n->op) {
case ONAME:
// ...
case OINDREG:
// Increase the refcount of the register so that igen's caller
// has to call regfree.
if(n->val.u.reg != D_SP)
reg[n->val.u.reg]++;
*a = *n;
return;
case ODOT:
igen(n->left, a, res);
a->xoffset += n->xoffset;
a->type = n->type;
return;
case ODOTPTR:
regalloc(a, types[tptr], res);
cgen(n->left, a);
if(n->xoffset != 0) {
// explicit check for nil if struct is large enough
// that we might derive too big a pointer.
if(n->left->type->type->width >= unmappedzero) {
n1 = *a;
n1.op = OINDREG;
n1.type = types[TUINT8];
n1.xoffset = 0;
gins(ATESTB, nodintconst(0), &n1);
}
}
a->op = OINDREG;
a->xoffset += n->xoffset;
a->type = n->type;
return;
// ...
}
このコードは、igen
関数が中間表現ノード n
を受け取り、そのノードが表す値を a
に設定するロジックを示しています。
-
OINDREG
ケース:n->val.u.reg != D_SP
の条件は、スタックポインタ以外のレジスタの場合に、そのレジスタの参照カウントreg[n->val.u.reg]
をインクリメントしています。これは、igen
がこのレジスタを「使用中」としてマークし、呼び出し元が後でregfree
を呼び出して解放する必要があることをコンパイラに伝えます。*a = *n;
は、入力ノードn
の情報を出力ノードa
にコピーします。これにより、a
はn
と同じレジスタ間接参照を表すようになります。
-
ODOT
ケース:igen(n->left, a, res);
は、構造体自体を表す左の子ノード (n->left
) を再帰的にigen
で処理し、その結果をa
に格納します。これにより、a
は構造体のベースアドレスまたはその参照を表すようになります。a->xoffset += n->xoffset;
は、a
が表すアドレスに、フィールドのオフセットn->xoffset
を加算します。これにより、a
は構造体フィールドの正確なアドレスを指すようになります。a->type = n->type;
は、a
の型をフィールドの型に設定します。
-
ODOTPTR
ケース:regalloc(a, types[tptr], res);
は、ポインタを格納するためのレジスタa
を確保します。cgen(n->left, a);
は、ポインタ変数自体を表す左の子ノード (n->left
) をcgen
で処理し、そのポインタ値をa
に格納します。if(n->xoffset != 0) { ... }
ブロックは、フィールドオフセットが0でない場合に、ポインタがnil
でないことを確認するための安全チェックを行います。if(n->left->type->type->width >= unmappedzero)
: ポインタが指す構造体のサイズがunmappedzero
(通常はメモリページのサイズ、例えば4KB) 以上の場合にチェックを行います。これは、大きな構造体の場合に、nil
ポインタをデリファレンスしようとした際に、アクセス違反が発生する前に検出するためのものです。n1 = *a; n1.op = OINDREG; n1.type = types[TUINT8]; n1.xoffset = 0;
は、a
が指すアドレスの0オフセットにあるバイトをテストするためのテンポラリノードn1
を作成します。gins(ATESTB, nodintconst(0), &n1);
は、ATESTB
(x86のTEST
命令に相当) 命令を生成し、n1
が指すメモリ位置のバイトを0と比較します。これにより、ポインタが有効なメモリを指しているか(nil
でないか)を間接的に確認します。
a->op = OINDREG;
は、a
のオペレーションをOINDREG
に設定し、a
がレジスタに格納されたポインタの間接参照を表すことを示します。a->xoffset += n->xoffset;
は、a
が指すアドレスにフィールドのオフセットを加算します。a->type = n->type;
は、a
の型をフィールドの型に設定します。
これらの変更により、コンパイラは構造体フィールドへのアクセスやポインタのデリファレンスを、より少ない命令で、かつレジスタを効率的に利用してアセンブリコードに変換できるようになりました。
src/cmd/8g/gsubr.c
のレジスタループ範囲変更
// 変更前
// for(i=D_AL; i<=D_DI; i++)
// 変更後
for(i=D_AX; i<=D_DI; i++)
D_AL
は AX
レジスタの下位8ビット (AL
) を指し、D_AX
は AX
レジスタ全体を指します。この変更は、レジスタ管理のループが、より広範なレジスタ(例えば、AX
レジスタ全体)を対象とするようになったことを示しています。これは、レジスタ割り当ての正確性を向上させるか、または特定のレジスタの利用方法が変更されたことによるものです。
関連リンク
- Go言語のコンパイラに関するドキュメントやソースコードは、Goの公式リポジトリで確認できます。
- GoのIssueトラッカー: https://github.com/golang/go/issues
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
参考にした情報源リンク
- Goコンパイラのソースコード (特に
src/cmd/gc/
,src/cmd/6g/
,src/cmd/8g/
ディレクトリ) - Go Issue #1914: https://github.com/golang/go/issues/1914 (コミットメッセージに記載されているIssue)
- Go CL 6494107: https://golang.org/cl/6494107 (コミットメッセージに記載されているコードレビューリンク)
- x86アセンブリ命令セットに関する資料 (LEAL/LEAQ 命令について)
- コンパイラ最適化に関する一般的な書籍やオンラインリソース (レジスタ割り当て、中間表現など)
- Go言語の
strconv
パッケージのドキュメント - Go言語のベンチマークに関するドキュメント
- Goコンパイラの歴史とアーキテクチャに関する記事 (例: Russ Cox のブログ記事など)