[インデックス 13691] ファイルの概要
このコミットは、Go言語のmath/bigパッケージ内のarith_amd64.sファイルに対する変更です。このファイルは、AMD64アーキテクチャ向けに最適化された多倍長整数演算(加算、減算など)のアセンブリコードを含んでいます。主な変更点は、コード内のコメントの修正と、bitLen関数における命令の変更です。
コミット
commit 74c6325142c4377527e1b9ee1ae06e489646e7ad
Author: Robert Griesemer <gri@golang.org>
Date: Fri Aug 24 13:50:09 2012 -0700
math/big: fix broken comment
R=iant, iant
CC=golang-dev
https://golang.org/cl/6485064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/74c6325142c4377527e1b9ee1ae06e489646e7ad
元コミット内容
このコミットの目的は、math/bigパッケージのAMD64アセンブリコード内の誤ったコメントを修正することです。具体的には、ループアンローリングを無効化する方法に関する説明が不正確であったため、これを正しい説明に修正しています。また、bitLen関数内の命令も変更されています。
変更の背景
math/bigパッケージは、Go言語で任意精度の算術演算を可能にするためのものです。パフォーマンスが非常に重要となるため、多くのコアな演算はアセンブリ言語で記述され、特定のCPUアーキテクチャ(この場合はAMD64)に最適化されています。
このコミットが行われた背景には、アセンブリコードの可読性と保守性の向上が挙げられます。特に、パフォーマンス最適化のために導入された「ループアンローリング」のデバッグやテストを行う際に、その機能を一時的に無効化する方法がコメントで示されていましたが、その説明が実際のコードの挙動と一致していませんでした。開発者がコードを理解し、必要に応じてデバッグやパフォーマンスチューニングを行う上で、正確なコメントは不可欠です。
また、INCLからADDL $1, AXへの変更は、特定のコンパイラやアセンブラの最適化、あるいは命令セットの利用効率に関する細かな調整である可能性があります。これは、Go言語のランタイムやコンパイラが進化する中で、より効率的または推奨される命令の使用に合わせた変更と考えられます。
前提知識の解説
1. Go言語のアセンブリ
Go言語は、一部のパフォーマンスクリティカルな部分でアセンブリ言語を使用します。これは、Goのコンパイラが生成するコードよりもさらに低レベルで、特定のCPU命令セットの特性を最大限に活用するためです。Goのアセンブリは、Plan 9アセンブラの文法に基づいており、一般的なIntel/AT&T構文とは異なる独自の記法を持ちます。
TEXT ·funcName(SB),7,$0: Goアセンブリにおける関数の宣言です。funcNameは関数名、(SB)はシンボルベース(Static Base)からの相対アドレス指定、7はフラグ(ここではNOSPLIT、スタックフレームを分割しないことを意味する)、$0はスタックフレームサイズを示します。MOVQ,ADDQ,SUBQ,JL,JMP: これらはAMD64アーキテクチャの基本的なアセンブリ命令です。MOVQ: 64ビットの値をレジスタまたはメモリ間で移動します。ADDQ: 64ビットの加算を行います。SUBQ: 64ビットの減算を行います。JL(Jump if Less): 直前の比較結果が「より小さい」場合にジャンプします。JMP(Jump): 無条件に指定されたアドレスにジャンプします。
INCL(Increment Long): 32ビットのレジスタまたはメモリの値を1増やします。ADDL $1, AX(Add Long): 32ビットのレジスタAXに即値1を加算します。
2. ループアンローリング (Loop Unrolling)
ループアンローリングは、プログラムの実行速度を向上させるためのコンパイラ最適化手法の一つです。ループの各イテレーションで発生するオーバーヘッド(ループカウンタのインクリメント、条件チェック、ジャンプ命令など)を削減するために、ループ本体を複数回展開し、1回のイテレーションでより多くの処理を行うようにします。
例えば、以下のC言語のループを考えます。
for (int i = 0; i < 10; i++) {
array[i] = i;
}
これを2回アンロールすると、以下のようになります。
for (int i = 0; i < 10; i += 2) {
array[i] = i;
array[i+1] = i+1;
}
これにより、ループの反復回数が半分になり、それに伴うオーバーヘッドも削減されます。しかし、コードサイズが増加したり、キャッシュの利用効率に影響を与えたりする可能性もあります。アセンブリコードでは、手動でループアンローリングを実装することがよくあります。
3. math/bigパッケージ
Go言語の標準ライブラリの一部であり、任意精度の整数(Int)、有理数(Rat)、浮動小数点数(Float)を扱うためのパッケージです。通常のGoの組み込み型(int, int64など)では表現できない非常に大きな数や、高い精度が求められる計算に使用されます。暗号化、科学技術計算、金融アプリケーションなどで利用されます。
技術的詳細
このコミットは、src/pkg/math/big/arith_amd64.sファイル内の2つの主要な変更を含んでいます。
1. コメントの修正
addVV, subVV, addVW, subVWといった関数内で、ループアンローリングを無効化するためのコメントが修正されました。
変更前:
// uncomment the next line to disable the unrolled loop
// JMP V1
このコメントは、「次の行のコメントを外せば、アンロールされたループを無効にできる」と指示しています。しかし、その次の行はJMP V1であり、実際のコードではJL V1という条件付きジャンプが使われています。もしJMP V1のコメントを外すと、無条件にV1にジャンプしてしまい、ループのロジックが完全に壊れてしまいます。
変更後:
// s/JL/JMP/ below to disable the unrolled loop
この新しいコメントは、「以下のJLをJMPに置換することで、アンロールされたループを無効にできる」と指示しています。これは、実際のコードがJL V1を使用していることを踏まえた、より正確な指示です。JLをJMPに置き換えることで、条件に関わらず常にジャンプするようになり、結果としてアンロールされたループの最適化パスをスキップし、通常のループ処理(または別のフォールバックパス)に移行させることができます。これは、デバッグやパフォーマンス比較の際に有用な手法です。
2. bitLen関数内の命令変更
bitLen関数内で、INCL AX命令がADDL $1, AX命令に変更されました。
変更前:
INCL AX
変更後:
ADDL $1, AX
両方の命令は、レジスタAXの値を1増やすという同じ論理的な結果をもたらします。しかし、これらの命令には微妙な違いがあります。
INCL:INC命令は、フラグレジスタ(特にキャリーフラグCF)を変更しないという特性を持つことがあります(ただし、他のフラグは変更します)。これは、特定の最適化や、フラグの状態を保持したい場合に利用されることがあります。ADDL $1, AX:ADD命令は、すべてのフラグレジスタ(CF, OF, SF, ZF, AF, PF)を変更します。
この変更の具体的な理由は、以下のいずれかである可能性があります。
- 一貫性: コードベース全体で
ADDL $1, regの形式が推奨されている場合、それに合わせるため。 - マイクロアーキテクチャの最適化: 特定のCPUマイクロアーキテクチャにおいて、
ADDL命令がINCLよりもパイプライン処理や実行ユニットの利用において効率的である場合。これは非常に低レベルな最適化であり、通常はコンパイラやアセンブラの設計者が決定します。 - フラグの挙動:
bitLen関数のコンテキストで、ADDLがフラグを変更する挙動が、後続の命令にとってより適切であるか、あるいはINCLのフラグ挙動が意図しない副作用を引き起こす可能性があったため。
Go言語のmath/bigパッケージは、パフォーマンスが非常に重要であるため、このような低レベルのアセンブリ命令の選択にも細心の注意が払われています。
コアとなるコードの変更箇所
src/pkg/math/big/arith_amd64.sファイルにおける変更は以下の通りです。
--- a/src/pkg/math/big/arith_amd64.s
+++ b/src/pkg/math/big/arith_amd64.s
@@ -34,9 +34,7 @@ TEXT ·addVV(SB),7,$0
MOVQ $0, CX // c = 0
MOVQ $0, SI // i = 0
- // uncomment the next line to disable the unrolled loop
- // JMP V1
-
+ // s/JL/JMP/ below to disable the unrolled loop
SUBQ $4, DI // n -= 4
JL V1 // if n < 0 goto V1
@@ -90,9 +88,7 @@ TEXT ·subVV(SB),7,$0
MOVQ $0, CX // c = 0
MOVQ $0, SI // i = 0
- // uncomment the next line to disable the unrolled loop
- // JMP V2
-
+ // s/JL/JMP/ below to disable the unrolled loop
SUBQ $4, DI // n -= 4
JL V2 // if n < 0 goto V2
@@ -144,9 +140,7 @@ TEXT ·addVW(SB),7,$0
MOVQ $0, SI // i = 0
- // uncomment the next line to disable the unrolled loop
- // JMP V3
-
+ // s/JL/JMP/ below to disable the unrolled loop
SUBQ $4, DI // n -= 4
JL V3 // if n < 4 goto V3
@@ -198,9 +192,7 @@ TEXT ·subVW(SB),7,$0
MOVQ $0, SI // i = 0
- // uncomment the next line to disable the unrolled loop
- // JMP V4
-
+ // s/JL/JMP/ below to disable the unrolled loop
SUBQ $4, DI // n -= 4
JL V4 // if n < 4 goto V4
@@ -389,7 +381,7 @@ E7: SUBL $1, BX // i--
TEXT ·bitLen(SB),7,$0
BSRQ x+0(FP), AX
JZ Z1
- INCL AX
+ ADDL $1, AX
MOVL AX, n+8(FP)
RET
コアとなるコードの解説
コメント修正部分
addVV, subVV, addVW, subVW関数は、それぞれ多倍長整数の加算・減算を行うためのアセンブリルーチンです。これらの関数は、パフォーマンス向上のためにループアンローリングを適用しています。
変更前のコメント// uncomment the next line to disable the unrolled loop // JMP V1は、開発者がアンローリングを無効にしたい場合に、JMP V1の行のコメントを外すように指示していました。しかし、実際のコードではJL V1(Jump if Less)という条件付きジャンプが使用されており、これはループの終了条件をチェックするためのものです。もしJMP V1のコメントを外してしまうと、ループの条件チェックを無視して無条件にジャンプしてしまい、プログラムの動作が不正になります。
新しいコメント// s/JL/JMP/ below to disable the unrolled loopは、この不正確さを修正しています。これは、開発者がJL命令をJMP命令に手動で置換することで、条件付きジャンプを無条件ジャンプに変更し、結果としてアンロールされたループの最適化パスをスキップして、別の(おそらく非最適化された)コードパスに強制的に移行させることを意図しています。これにより、デバッグやパフォーマンスの比較がより安全かつ正確に行えるようになります。
bitLen関数内の命令変更部分
bitLen関数は、与えられた数値のビット長(最上位ビットの位置)を計算する関数です。
BSRQ x+0(FP), AX:BSR(Bit Scan Reverse) 命令は、オペランドの最上位セットビット(1になっているビット)のインデックスを検索し、そのインデックスをデスティネーションオペランドに格納します。x+0(FP)は、関数の引数xがスタックフレームポインタFPからのオフセットで参照されることを示します。結果はAXレジスタに格納されます。JZ Z1:JZ(Jump if Zero) 命令は、直前の演算結果がゼロであった場合にZ1ラベルにジャンプします。BSRQ命令は、入力がゼロの場合にゼロフラグ(ZF)をセットします。したがって、入力がゼロであればZ1にジャンプします。INCL AXからADDL $1, AXへの変更:BSRQ命令は0-indexedのビット位置を返します。例えば、0b100(4)のBSRQ結果は2です(0, 1, 2番目のビット)。しかし、ビット長は通常1-indexedで数えられます(この場合3ビット)。そのため、結果に1を加算する必要があります。INCL AXはAXレジスタの値を1増やします。ADDL $1, AXもAXレジスタの値を1増やします。 この変更は、前述の通り、命令セットの利用効率、コンパイラの最適化、またはフラグの挙動に関する細かな調整である可能性が高いです。Goのアセンブリコードでは、このような低レベルの最適化が頻繁に行われます。
関連リンク
- Go言語の
math/bigパッケージのドキュメント: https://pkg.go.dev/math/big - Go言語のアセンブリに関する公式ドキュメント: https://go.dev/doc/asm
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/6485064
参考にした情報源リンク
- https://go.dev/doc/asm
- https://pkg.go.dev/math/big
- Intel 64 and IA-32 Architectures Software Developer's Manuals (特にVol. 2A: Instruction Set Reference, A-M)
- ループアンローリングに関する一般的な情報源 (例: Wikipedia, コンパイラ最適化に関する書籍)
INCLvsADDLに関するアセンブリプログラミングの議論 (例: Stack Overflow, フォーラム)