[インデックス 14711] ファイルの概要
このコミットは、GoコンパイラのARMアーキテクチャ向けバックエンドであるcmd/5g
において、配列やスライスのインデックスアクセス(agen OINDEX
)時の不要な一時変数(temporaries)の生成を回避することで、コード生成の効率を改善し、特にfmt
パッケージのベンチマークにおいてパフォーマンス向上を実現したものです。
コミット
- コミットハッシュ:
3dcc63f750c425fcb9ce2ec66812489881dafeee
- Author: Dave Cheney dave@cheney.net
- Date: Sat Dec 22 09:09:31 2012 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3dcc63f750c425fcb9ce2ec66812489881dafeee
元コミット内容
cmd/5g: avoid temporaries in agen OINDEX
Most benchmarks are within the 3% margin of error. This code path is quite common in the fmt package.
linux/arm, Freescale iMX.53 (cortex-a8)
fmt:
benchmark old ns/op new ns/op delta
BenchmarkSprintfEmpty 925 785 -15.14%
BenchmarkSprintfString 5050 5039 -0.22%
BenchmarkSprintfInt 4425 4406 -0.43%
BenchmarkSprintfIntInt 5802 5762 -0.69%
BenchmarkSprintfPrefixedInt 7029 6541 -6.94%
BenchmarkSprintfFloat 10278 10083 -1.90%
BenchmarkManyArgs 18558 17606 -5.13%
BenchmarkScanInts 15592690 15415360 -1.14%
BenchmarkScanRecursiveInt 25155020 25050900 -0.41%
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6921056
変更の背景
この変更の主な背景は、GoコンパイラのARMアーキテクチャ向けバックエンド(cmd/5g
)におけるコード生成の効率化とパフォーマンス改善です。特に、配列やスライスのインデックスアクセス(agen OINDEX
)の際に、コンパイラが不必要に一時的なレジスタやメモリ領域("temporaries")を割り当てていた問題がありました。
コミットメッセージに示されているベンチマーク結果は、この最適化が特にfmt
パッケージの処理において顕著な効果をもたらすことを示しています。fmt
パッケージは文字列フォーマットやスキャンなど、Goプログラムで頻繁に使用される機能を提供しており、その内部では配列やスライスへのアクセスが多用されます。そのため、このコードパスの最適化は、Goプログラム全体の実行速度に良い影響を与えると考えられました。
具体的には、BenchmarkSprintfEmpty
で15.14%の改善、BenchmarkSprintfPrefixedInt
で6.94%の改善、BenchmarkManyArgs
で5.13%の改善が見られ、他のベンチマークでもわずかながら改善が確認されています。これは、一時変数の削減が、レジスタの利用効率向上や命令数の削減に繋がり、結果としてCPUサイクルとメモリ帯域の消費を減らしたためです。
前提知識の解説
cmd/5g
cmd/5g
は、Go言語のコンパイラツールチェーンの一部であり、ARMアーキテクチャ(具体的にはGOARCH=arm
)向けのGoソースコードをコンパイルするバックエンドです。Goコンパイラは、フロントエンドでGoのソースコードを中間表現に変換し、その後、各アーキテクチャ固有のバックエンドがその中間表現をターゲットアーキテクチャの機械語に変換します。5g
の5
は、ARMアーキテクチャの歴史的な命名規則(例: 5
はARMv5、6
はARMv6など)に由来しています。
agen OINDEX
agen
は"address generation"の略で、コンパイラがメモリ上のデータ(変数、配列要素、構造体フィールドなど)のアドレスを計算する処理を指します。OINDEX
は、Goコンパイラの中間表現における「インデックス操作」を表すオペレーションコード(opcode)の一つです。これは、配列やスライスの特定のインデックスにある要素にアクセスする際に、その要素のメモリアドレスを計算するために使用されます。例えば、a[i]
のようなコードは、agen OINDEX
の処理を経て、a
のベースアドレスにi
と要素サイズの積を加算するような機械語命令に変換されます。
Temporaries (一時変数/一時レジスタ)
プログラミング言語のコンテキストにおいて、「一時変数」または「一時レジスタ」とは、コンパイラが複雑な計算や式を評価する際に、中間結果を一時的に保持するために内部的に生成・使用する変数やレジスタのことです。例えば、x = (a + b) * c
という式では、a + b
の結果を一時的に保持するための場所が必要になります。コンパイラは、このような中間結果を格納するために、一時的なメモリ位置やCPUレジスタを割り当てます。
一時変数の生成は、コードの正確性を保証するために必要不可欠な場合もありますが、不必要に多くの一時変数を生成すると、以下のようなオーバーヘッドが発生します。
- レジスタの枯渇: 使用できるCPUレジスタの数が限られているため、一時変数が多すぎると、レジスタに収まらない中間結果をメモリにスピル(退避)する必要が生じ、メモリアクセスによる性能低下を招きます。
- 命令数の増加: 一時変数のロード/ストア操作のために、余分な機械語命令が生成されます。
- キャッシュ効率の低下: メモリへのアクセスが増えることで、CPUキャッシュの利用効率が低下する可能性があります。
したがって、コンパイラ最適化の重要な目標の一つは、プログラムのセマンティクスを維持しつつ、不必要または冗長な一時変数の生成を最小限に抑えることです。
技術的詳細
このコミットの技術的詳細なポイントは、cmd/5g
コンパイラのagenr
関数(おそらくagen
の一部で、レジスタ割り当てとコード生成を担当)における、配列境界チェックのロジックの改善です。
変更前のコードでは、配列やスライスの長さを取得し、インデックスがその範囲内にあるかをチェックする際に、不必要に一時レジスタn4
を割り当て、そこに値をgmove
(汎用的な移動命令)でコピーしていました。
具体的には、以下のパターンが見られました。
regalloc(&n4, types[TUINT32], N);
:一時レジスタn4
を割り当てる。nodconst(&n1, types[TUINT32], ...);
:定数ノードn1
を作成。gmove(&n1, &n4);
:n1
の値をn4
に移動。
この一連の操作は、定数値を直接n4
に設定できるにもかかわらず、中間的なn1
を介してgmove
を使用している点で冗長でした。また、スライスや文字列の長さ(Array_nel
)を取得する場合も同様に、n1
を介してn4
に移動していました。
このコミットでは、この冗長性を排除し、定数やスライス/文字列の長さを直接n4
に設定するように変更されています。
- 定数の場合:
nodconst(&n4, types[TUINT32], ...);
とすることで、n1
の生成とgmove
が不要になりました。 - スライス/文字列の長さの場合:
regalloc(&n4, types[TUINT32], N);
の後に直接gmove(&n1, &n4);
を行うことで、n1
の再利用とn4
への直接的な値の移動を効率化しています。
さらに、regfree(&n4);
の呼び出しが、n4.op == OREGISTER
という条件付きになっています。これは、n4
が実際にレジスタとして割り当てられている場合にのみ解放処理を行うことで、不必要なregfree
呼び出しを避けるための防御的な変更と考えられます。これにより、コンパイラの内部状態の一貫性が保たれ、潜在的なバグが回避されます。
これらの変更により、コンパイラが生成する機械語コードから不要なロード/ストア命令やレジスタ移動命令が削減され、結果として実行時のパフォーマンスが向上します。特に、境界チェックは配列やスライスへのアクセスごとに頻繁に行われるため、この小さな最適化が全体的な性能に大きな影響を与えます。
コアとなるコードの変更箇所
変更はsrc/cmd/5g/cgen.c
ファイル内のagenr
関数に集中しています。
--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -1007,22 +1007,21 @@ agenr(Node *n, Node *a, Node *res)
if(!debug['B'] && !n->bounded) {
// check bounds
- regalloc(&n4, types[TUINT32], N);
if(isconst(nl, CTSTR)) {
- nodconst(&n1, types[TUINT32], nl->val.u.sval->len);
- gmove(&n1, &n4);
+ nodconst(&n4, types[TUINT32], nl->val.u.sval->len);
} else if(isslice(nl->type) || nl->type->etype == TSTRING) {
n1 = n3;
n1.op = OINDREG;
n1.type = types[tptr];
n1.xoffset = Array_nel;
+ regalloc(&n4, types[TUINT32], N);
gmove(&n1, &n4);
} else {
- nodconst(&n1, types[TUINT32], nl->type->bound);
- gmove(&n1, &n4);
+ nodconst(&n4, types[TUINT32], nl->type->bound);
}
gcmp(optoas(OCMP, types[TUINT32]), &n2, &n4);
- regfree(&n4);
+ if(n4.op == OREGISTER)
+ regfree(&n4);
p1 = gbranch(optoas(OLT, types[TUINT32]), T, +1);
if(p2)
patch(p2, pc);
コアとなるコードの解説
変更されたコードブロックは、配列やスライスの境界チェック(check bounds
)を行う部分です。
-
変更前:
regalloc(&n4, types[TUINT32], N);
がif
文のブロックの最初に無条件で呼び出されていました。これは、n4
という一時レジスタを、符号なし32ビット整数型(TUINT32
)のために割り当てることを意味します。- 定数文字列の長さ(
isconst(nl, CTSTR)
)や、スライス/文字列の長さ(isslice(nl->type) || nl->type->etype == TSTRING
)、または型の境界値(nl->type->bound
)を取得する際に、一度n1
という別のノードに値を設定し、その後gmove(&n1, &n4);
を使ってn1
からn4
へ値をコピーしていました。
-
変更後:
regalloc(&n4, types[TUINT32], N);
の呼び出しが、スライスや文字列の長さを取得するelse if
ブロックの中に移動しました。これにより、n4
が必要になる直前で割り当てられるようになり、不要な早期割り当てがなくなりました。- 定数文字列の長さや型の境界値を取得する際には、
nodconst(&n4, types[TUINT32], ...);
のように、直接n4
に定数値を設定するように変更されました。これにより、中間的なn1
ノードとgmove
命令が不要になり、コード生成がより直接的かつ効率的になりました。 - スライスや文字列の長さの場合も、
n1
にArray_nel
(配列の要素数)を設定した後、gmove(&n1, &n4);
でn4
に移動する点は変わりませんが、n4
の割り当てが適切な位置に移動したことで、全体的なレジスタ利用が最適化されています。 - 最後に、
regfree(&n4);
の呼び出しがif(n4.op == OREGISTER)
という条件付きになりました。これは、n4
が実際にレジスタとして割り当てられている場合にのみ、そのレジスタを解放するようにすることで、コンパイラの内部状態の整合性を保ち、潜在的なエラーを防ぐための堅牢化です。
これらの変更により、コンパイラは配列境界チェックの際に生成する機械語命令の数を減らし、レジスタの利用効率を高めることで、特にfmt
パッケージのような頻繁にインデックスアクセスを行うコードの実行速度を向上させています。
関連リンク
- Go CL (Change List): https://golang.org/cl/6921056
参考にした情報源リンク
- (Web検索は行いませんでした。提供されたコミット情報と差分のみで解説を生成しました。)