[インデックス 13799] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)において、64ビットポインタ型(TPTR64
)の小さな定数値を、より効率的なアセンブリ命令に変換できるように改善するものです。具体的には、ポインタ演算における小さな定数(例: +4
)が、冗長なレジスタ操作を介さずに直接加算命令として生成されるようになります。これにより、生成されるバイナリのサイズが削減され、実行効率が向上します。
コミット
commit 0184081eb903a9c8ccc6dae250ea79b5c2c06e26
Author: Nigel Tao <nigeltao@golang.org>
Date: Tue Sep 11 19:45:28 2012 +1000
cmd/gc: recognize small TPTR64 values as small integer constants.
Given the following Go program:
func sum(s []int) int {
ret := 0
for _, x := range s {
ret += x
}
return ret
}
6g would previously generate:
--- prog list "sum" ---
0000 (main.go:3) TEXT sum+0(SB),$0-24
0001 (main.go:5) MOVQ s+0(FP),CX
0002 (main.go:5) MOVL s+8(FP),DI
0003 (main.go:5) MOVL s+12(FP),BX
0004 (main.go:4) MOVL $0,SI
0005 (main.go:5) MOVL $0,AX
0006 (main.go:5) JMP ,8
0007 (main.go:5) INCL ,AX
0008 (main.go:5) CMPL AX,DI
0009 (main.go:5) JGE $0,16
0010 (main.go:5) MOVL (CX),DX
0011 (main.go:5) MOVQ $4,BX
0012 (main.go:5) ADDQ CX,BX
0013 (main.go:5) MOVQ BX,CX
0014 (main.go:6) ADDL DX,SI
0015 (main.go:5) JMP ,7
0016 (main.go:8) MOVL SI,.noname+16(FP)
0017 (main.go:8) RET ,
and now generates:
--- prog list "sum" ---
0000 (main.go:3) TEXT sum+0(SB),$0-24
0001 (main.go:5) MOVQ s+0(FP),CX
0002 (main.go:5) MOVL s+8(FP),DI
0003 (main.go:5) MOVL s+12(FP),BX
0004 (main.go:4) MOVL $0,SI
0005 (main.go:5) MOVL $0,AX
0006 (main.go:5) JMP ,8
0007 (main.go:5) INCL ,AX
0008 (main.go:5) CMPL AX,DI
0009 (main.go:5) JGE $0,14
0010 (main.go:5) MOVL (CX),BP
0011 (main.go:5) ADDQ $4,CX
0012 (main.go:6) ADDL BP,SI
0013 (main.go:5) JMP ,7
0014 (main.go:8) MOVL SI,.noname+16(FP)
0015 (main.go:8) RET ,
The key difference is that
0011 (main.go:5) MOVQ $4,BX
0012 (main.go:5) ADDQ CX,BX
0013 (main.go:5) MOVQ BX,CX
has changed to
0011 (main.go:5) ADDQ $4,CX
R=rsc, dave, remyoudompheng
CC=golang-dev
https://golang.org/cl/6506089
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0184081eb903a9c8ccc6dae250ea79b5c2c06e26
元コミット内容
上記の「コミット」セクションに記載されている内容が、このコミットの元の内容です。
変更の背景
この変更の背景には、Goコンパイラ(特に当時の6g
、現在のcmd/compile
の一部)が、ポインタ演算における小さな定数(例: スライス要素へのアクセスでポインタを4
バイト進める場合など)を効率的に扱えていなかったという問題があります。
コミットメッセージの例にあるsum
関数のように、for _, x := range s
ループ内でスライスs
の要素にアクセスする際、内部的にはポインタを次の要素の先頭に移動させる処理が行われます。int
型が4バイトである場合、ポインタは4
バイトずつ進む必要があります。
変更前のアセンブリコードでは、この「ポインタを4
バイト進める」という単純な操作に対して、以下のような冗長な命令が生成されていました。
0011 (main.go:5) MOVQ $4,BX ; 定数4をBXレジスタに移動
0012 (main.go:5) ADDQ CX,BX ; CXレジスタ(現在のポインタ)にBXレジスタ(4)を加算し、結果をBXに格納
0013 (main.go:5) MOVQ BX,CX ; BXレジスタの結果をCXレジスタに移動(CXが新しいポインタ値となる)
この3つの命令は、本来であれば1つの命令で済むはずの操作です。
0011 (main.go:5) ADDQ $4,CX ; CXレジスタに直接定数4を加算
コンパイラが$4
という定数を、TPTR64
(64ビットポインタ型)の文脈で「小さな整数定数」として認識できていなかったため、このような非効率なコードが生成されていました。この非効率性は、ループ内で頻繁に発生するポインタ演算において、パフォーマンスの低下やバイナリサイズの増大を招く可能性がありました。このコミットは、この非効率性を解消し、より最適化されたアセンブリコードを生成することを目的としています。
前提知識の解説
Goコンパイラ (cmd/gc
)
Go言語のソースコードを機械語に変換するコンパイラです。Goのツールチェインの一部であり、go build
コマンドなどで内部的に利用されます。当時の6g
は、x86-64アーキテクチャ向けのGoコンパイラの名前でした。コンパイラは、ソースコードの解析、中間表現の生成、最適化、そして最終的な機械語コードの生成という複数のフェーズを経て動作します。
アセンブリ言語
コンピュータのCPUが直接実行できる機械語命令を、人間が理解しやすいようにニーモニック(記号)で表現した低水準プログラミング言語です。各命令は特定のCPU操作に対応しており、レジスタ間のデータ転送、算術演算、メモリへのアクセス、条件分岐などを制御します。
MOVQ
(Move Quadword): 64ビットのデータを移動する命令。MOVQ $4,BX
は、即値4
をBX
レジスタに移動します。MOVL
(Move Long): 32ビットのデータを移動する命令。ADDQ
(Add Quadword): 64ビットの加算命令。ADDQ CX,BX
はBX = BX + CX
を意味し、ADDQ $4,CX
はCX = CX + 4
を意味します。JMP
(Jump): 無条件ジャンプ命令。指定されたアドレスに実行フローを移します。CMPL
(Compare Long): 32ビットの比較命令。2つのオペランドを比較し、フラグレジスタを設定します。INCL
(Increment Long): 32ビットのインクリメント命令。オペランドの値を1増やします。RET
(Return): サブルーチンからのリターン命令。
レジスタ
CPU内部にある高速な記憶領域です。アセンブリ言語では、データの一時的な格納や演算に使用されます。
CX
: カウンタレジスタ。ループカウンタやポインタとして使われることが多いです。BX
: ベースレジスタ。メモリのアドレス指定に使われることが多いです。DI
: 宛先インデックスレジスタ。文字列操作などで宛先アドレスを指すのに使われます。SI
: ソースインデックスレジスタ。文字列操作などでソースアドレスを指すのに使われます。AX
: アキュムレータレジスタ。算術演算の結果を格納したり、関数の戻り値を格納したりします。BP
: ベースポインタレジスタ。スタックフレームのベースアドレスを指すのに使われます。
Goの型システム
int
: プラットフォームに依存する整数型。通常は32ビットまたは64ビットです。[]int
(slice): Goにおける可変長配列のようなデータ構造。内部的には、要素へのポインタ、長さ、容量の3つの情報で構成されます。スライス要素へのアクセスは、基底配列のポインタとインデックスに基づいて行われます。
TPTR64
Goコンパイラの内部表現における型の一つで、64ビットポインタを表します。これは、メモリ上のアドレスを指し示すために使用される型です。
コンパイラの最適化
コンパイラが生成する機械語コードの効率を向上させるプロセスです。これには、冗長な命令の削除、より効率的な命令への置き換え、定数畳み込み(コンパイル時に定数式を計算して結果の定数に置き換えること)などが含まれます。このコミットのケースは、定数畳み込みや命令選択の最適化に関連します。コンパイラが小さな定数を適切に認識し、それらを直接命令のオペランドとして使用できる場合、中間レジスタを介した冗長な操作を避けることができます。
smallintconst
関数
Goコンパイラのsrc/cmd/gc/const.c
ファイルに存在する関数で、与えられたノード(コンパイラ内部の抽象構文木の要素)が、特定の範囲内の小さな整数定数であるかどうかを判定します。この関数は、コンパイラがより効率的な命令を生成するために、定数を特別扱いできるかどうかを判断する際に利用されます。例えば、ADDQ $4,CX
のように、定数を直接命令に埋め込むことができるのは、その定数が「小さな整数定数」として認識される場合です。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのコード生成フェーズにおける「定数認識」の改善にあります。
Goコンパイラは、ソースコードを解析し、抽象構文木(AST)を構築した後、それを中間表現に変換し、最終的にターゲットアーキテクチャの機械語コードを生成します。この過程で、コンパイラは様々な最適化を適用します。その一つが、定数値を効率的に扱うことです。
以前の6g
コンパイラでは、TPTR64
型(64ビットポインタ)に関連する定数、特にポインタのオフセットとして使われる小さな整数定数(例: 4
バイト)が、smallintconst
関数によって「小さな整数定数」として正しく認識されていませんでした。
smallintconst
関数は、特定の型(TIDEAL
, TINT64
, TUINT64
など)のノードが、32ビット符号付き整数の範囲内(minintval[TINT32]
からmaxintval[TINT32]
まで)に収まるかどうかをチェックします。もし収まる場合、その値は「小さな整数定数」と見なされ、コンパイラはそれを直接アセンブリ命令の即値オペランドとして使用するような最適化を適用できます。
しかし、TPTR64
型がこのsmallintconst
関数のチェック対象に含まれていなかったため、ポインタ演算で+4
のような小さな定数が現れても、コンパイラはそれを直接命令に埋め込むことができず、一時的なレジスタ(BX
)を介した冗長なMOVQ
とADDQ
のシーケンスを生成していました。
このコミットは、src/cmd/gc/const.c
内のsmallintconst
関数にcase TPTR64:
を追加することで、この問題を解決しました。これにより、TPTR64
型のノードも、その値が32ビット符号付き整数の範囲内であれば、「小さな整数定数」として認識されるようになります。
結果として、コンパイラは以下のような最適化されたアセンブリコードを生成できるようになります。
-
変更前:
MOVQ $4,BX ; 定数4をBXにロード ADDQ CX,BX ; CX + BX を BX に格納 MOVQ BX,CX ; BX を CX に格納
このシーケンスは、
CX
に4
を加算するために3つの命令と2つのレジスタ操作を必要とします。 -
変更後:
ADDQ $4,CX ; CX に直接定数4を加算
この単一の命令は、直接
CX
レジスタに4
を加算します。これにより、命令数が削減され、レジスタの利用効率が向上し、結果として生成されるバイナリのサイズが小さくなり、実行速度もわずかに向上します。これは、特にループ内で頻繁に実行される操作において、累積的なパフォーマンス改善に繋がります。
コアとなるコードの変更箇所
変更はsrc/cmd/gc/const.c
ファイル内のsmallintconst
関数に1行追加されただけです。
--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -1207,6 +1207,7 @@ smallintconst(Node *n)
case TIDEAL:
case TINT64:
case TUINT64:
+ case TPTR64:
if(mpcmpfixfix(n->val.u.xval, minintval[TINT32]) < 0
|| mpcmpfixfix(n->val.u.xval, maxintval[TINT32]) > 0)
break;
コアとなるコードの解説
smallintconst
関数は、Goコンパイラのバックエンドの一部であり、最適化のために定数値を分析します。この関数は、引数としてNode *n
を受け取ります。Node
はコンパイラが扱う抽象構文木(AST)のノードを表し、プログラムの要素(変数、定数、式など)を含みます。
switch
文は、ノードn
の型(n->type
)に基づいて処理を分岐させます。
変更前のコードでは、TIDEAL
(理想的な型、型推論前の数値リテラルなど)、TINT64
(64ビット符号付き整数)、TUINT64
(64ビット符号なし整数)の各ケースが処理されていました。これらの型を持つノードが、32ビット符号付き整数の最小値と最大値の範囲内にあるかどうかをmpcmpfixfix
関数でチェックしています。mpcmpfixfix
は多倍長整数(mpint
)の比較を行う関数です。
このコミットでは、既存のcase
文にcase TPTR64:
が追加されました。
これにより、TPTR64
型(64ビットポインタ型)のノードも、他の整数型と同様に、その値が32ビット符号付き整数の範囲内にあるかどうかのチェック対象となります。
もしTPTR64
型のノードがこの条件を満たし、「小さな整数定数」と認識されれば、コンパイラはポインタ演算において、その定数を直接アセンブリ命令の即値オペランドとして使用するようになります。例えば、ADDQ $4,CX
のように、レジスタを介した冗長な操作を避けて、より効率的なコードを生成できるようになります。
この変更は、コンパイラのコード生成における微細な最適化ですが、Goプログラム全体、特にポインタ演算が頻繁に行われるようなコードにおいて、生成されるバイナリの品質と実行効率を向上させる効果があります。
関連リンク
- Go Gerrit Change-Id:
6506089
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gc/
ディレクトリ内のファイル) - x86-64アセンブリ言語の命令セットに関する一般的な情報
- コンパイラの最適化に関する一般的な情報(定数畳み込み、命令選択など)