Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14785] ファイルの概要

このコミットは、Goコンパイラ(具体的にはx86アーキテクチャ向けのcmd/8g)における一時変数(テンポラリ)の最適化に関するものです。特に、SSE2(Streaming SIMD Extensions 2)命令セットを使用するコードにおいて、一時変数の排除を拡張し、生成されるアセンブリコードのサイズ削減と実行速度の向上を実現しています。

コミット

commit cf77dd37e7e7ae8eefe8c21a9ff3f04de989b808
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Jan 3 00:44:31 2013 +0100

    cmd/8g: extend elimination of temporaries to SSE2 code.
    
    Before:
    (erf.go:188)    TEXT     Erf+0(SB),$220
    (erf.go:265)    TEXT     Erfc+0(SB),$204
    (lgamma.go:174) TEXT     Lgamma+0(SB),$948
    
    After:
    (erf.go:188)    TEXT     Erf+0(SB),$84
    (erf.go:265)    TEXT     Erfc+0(SB),$84
    (lgamma.go:174) TEXT     Lgamma+0(SB),$44
    
    SSE before vs. SSE after:
    
    benchmark             old ns/op    new ns/op    delta
    BenchmarkAcosh               81           49  -39.14%
    BenchmarkAsinh              109          109   +0.00%
    BenchmarkAtanh               73           74   +0.68%
    BenchmarkLgamma             138           42  -69.20%
    BenchmarkModf                24           15  -36.95%
    BenchmarkSqrtGo             565          556   -1.59%
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7028048

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cf77dd37e7e7ae8eefe8c21a9ff3f04de989b808

元コミット内容

このコミットは、Goコンパイラのcmd/8gにおいて、一時変数の排除ロジックをSSE2コードにまで拡張するものです。変更前と変更後で、ErfErfcLgammaといった数学関数のアセンブリコードサイズが大幅に削減されていることが示されています。

具体的には、Erf関数は220バイトから84バイトへ、Erfc関数は204バイトから84バイトへ、Lgamma関数は948バイトから44バイトへと、それぞれ大幅にコードサイズが減少しています。

さらに、ベンチマーク結果も示されており、BenchmarkAcoshで39.14%の高速化、BenchmarkLgammaで69.20%の高速化、BenchmarkModfで36.95%の高速化が達成されています。これは、一時変数の効率的な管理が、単にコードサイズを減らすだけでなく、実行時のパフォーマンスにも直接的な影響を与えることを示しています。

変更の背景

コンパイラ最適化における「一時変数の排除(elimination of temporaries)」は、プログラムの実行効率を高める上で非常に重要な手法です。一時変数とは、計算の中間結果を保持するためにコンパイラが内部的に生成する変数であり、これらがメモリ上に不必要に割り当てられると、メモリ帯域の消費やキャッシュミスの増加、さらにはガベージコレクションのオーバーヘッド増大につながります。

Goコンパイラは、特にパフォーマンスが重視されるシステムプログラミング言語であるため、生成されるバイナリのサイズと実行速度の両面で最適化が求められます。SSE2(Streaming SIMD Extensions 2)は、x86アーキテクチャにおけるSIMD(Single Instruction, Multiple Data)命令セットであり、浮動小数点演算や整数演算を並列に処理する能力を提供します。SSE2を効率的に利用するためには、中間結果をCPUレジスタに可能な限り保持し、メモリへの不必要な読み書き(スピル)を避けることが極めて重要です。

このコミットの背景には、cmd/8gコンパイラがSSE2コードを生成する際に、まだ最適化の余地がある一時変数の扱いを改善し、よりコンパクトで高速なコードを生成するという目的があったと考えられます。特に、数学ライブラリのような浮動小数点演算を多用するコードでは、一時変数の効率的な管理がパフォーマンスに直結します。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  • Goコンパイラ (cmd/8g):
    • かつてGo言語のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っていました。cmd/8gは、x86 (386) アーキテクチャ向けのGoコンパイラを指します。現代のGoツールチェインでは、go tool compileコマンドが統一的に使用され、GOARCH環境変数によってターゲットアーキテクチャが指定されます(例: GOARCH=386)。このコミットは、Goコンパイラの初期の段階における最適化の取り組みを示しています。
  • 一時変数 (Temporaries):
    • プログラミングにおいて、一時変数とは、計算の中間結果を一時的に格納するために使用される変数のことです。コンパイラは、ソースコードを機械語に変換する過程で、これらの内部的な一時変数を多数生成します。これらの変数を効率的に管理し、不要なものを排除することは、生成されるコードのサイズを削減し、実行速度を向上させる上で不可欠です。
  • SSE2 (Streaming SIMD Extensions 2):
    • Intelが開発したx86アーキテクチャ用のSIMD命令セットです。SIMDは、単一の命令で複数のデータ要素に対して同じ操作を同時に実行する能力を指します。SSE2は、特に浮動小数点演算(単精度および倍精度)と整数演算のパフォーマンス向上を目的としており、グラフィックス、科学計算、マルチメディア処理などで広く利用されます。SSE2命令は、専用のXMMレジスタ(128ビット幅)を使用します。
  • コンパイラ最適化:
    • コンパイラ最適化とは、コンパイラがソースコードを機械語に変換する際に、生成されるコードの実行速度、サイズ、消費電力などを改善するプロセスです。一時変数の排除は、レジスタ割り当て、デッドコード排除、共通部分式削除など、様々な最適化手法と密接に関連しています。
  • レジスタ割り当て (Register Allocation):
    • コンパイラ最適化の重要なフェーズの一つで、プログラムの変数や中間結果をCPUの高速なレジスタに割り当てるプロセスです。レジスタはメモリよりもはるかに高速にアクセスできるため、レジスタに多くの値を保持できるほど、プログラムの実行速度は向上します。レジスタが不足した場合、値はメモリ(スタック)に「スピル(spill)」されますが、これはパフォーマンス低下の原因となります。
  • スタックフレーム (Stack Frame):
    • 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどを格納するために、コールスタック上に確保されるメモリ領域です。一時変数がスタックに割り当てられると、メモリへのアクセスが必要となり、レジスタに保持されるよりも遅くなります。
  • Prog構造体:
    • Goコンパイラの内部で、アセンブリ命令を表現するために使用されるデータ構造です。これはGo言語の公開APIの一部ではなく、コンパイラの内部実装の詳細です。
  • D_AUTO:
    • Goコンパイラの内部で、自動変数(スタックに割り当てられるローカル変数)を示すために使用される型です。これもコンパイラの内部表現であり、Go言語の標準的な型システムには存在しません。
  • RtoBおよびFtoB関数:
    • Goコンパイラの内部関数で、レジスタ(Register)からブロック(Block)へ、またはフレーム(Frame、スタック)からブロックへ、といったデータフローやメモリ管理に関連する操作を示唆していると考えられます。これらもコンパイラの内部実装の詳細です。
  • AMOVSS, AMOVSD, AMOVB, AMOVW, AMOVL命令:
    • これらはGoコンパイラの内部的なアセンブリ命令表現です。Goのアセンブリ言語はPlan 9アセンブラに基づいています。
      • AMOVB: 1バイト (8ビット) の移動
      • AMOVW: 1ワード (16ビット) の移動
      • AMOVL: 1ロングワード (32ビット) の移動
      • AMOVSS: 単精度浮動小数点数 (32ビット) の移動 (SSE命令 MOVSSに対応)
      • AMOVSD: 倍精度浮動小数点数 (64ビット) の移動 (SSE命令 MOVSDに対応)
    • これらの命令は、データサイズと型に応じて適切な移動操作を示します。
  • isfloat配列:
    • Goコンパイラの内部で、特定の型が浮動小数点型であるかどうかを判定するために使用されるフラグまたは配列であると推測されます。

技術的詳細

このコミットの核心は、src/cmd/8g/reg.cファイル内のfixtemp関数に対する変更です。fixtemp関数は、Goコンパイラのバックエンドの一部であり、一時変数の最適化、特にレジスタ割り当てとメモリ(スタック)へのスピルを管理する役割を担っています。

変更前は、fixtemp関数は浮動小数点型の一時変数に対しては、一時変数の排除を適用していませんでした。これは、isfloat[p->to.etype]という条件によって、浮動小数点型の場合に処理をスキップしていたためです。

変更後では、この条件が緩和され、浮動小数点型の一時変数に対しても最適化が適用されるようになりました。具体的には、p->to.type != D_AUTOという条件はそのままに、isfloat[p->to.etype]が真である場合に、AMOVSS(単精度浮動小数点数移動)またはAMOVSD(倍精度浮動小数点数移動)命令であるかをチェックするようになりました。これにより、SSE2命令で扱われる浮動小数点型の一時変数も、レジスタ割り当ての対象となり、不要なメモリへのスピルが削減されるようになりました。

また、浮動小数点型ではない一時変数に対しても、RtoB(p->from.type)(レジスタからブロックへの移動)という条件が追加され、AMOVBAMOVWAMOVLといった整数/汎用レジスタの移動命令に対して最適化が適用されるようになりました。

この変更により、コンパイラはより積極的に一時変数をレジスタに保持しようと試み、結果としてスタックフレームの使用量を削減し、生成されるアセンブリコードのサイズを縮小します。ベンチマーク結果が示すように、これは特に浮動小数点演算を多用する関数において、顕著なパフォーマンス向上をもたらします。

コアとなるコードの変更箇所

変更はsrc/cmd/8g/reg.cファイルのfixtemp関数内で行われています。

--- a/src/cmd/8g/reg.c
+++ b/src/cmd/8g/reg.c
@@ -1845,26 +1845,33 @@ fixtemp(Prog *firstp)
 	for(p=firstp; p!=P; p=p->link) {
 		if(debug['R'] && debug['v'])
 			print("%P\n", p);
-		if(p->link == P
-			|| !RtoB(p->from.type)
-			|| p->to.type != D_AUTO
-			|| isfloat[p->to.etype])
+		if(p->link == P || p->to.type != D_AUTO)
 			continue;
-		switch(p->as) {
-		case AMOVB:
-			if(p->to.width == 1)
-				break;
-		case AMOVW:
-			if(p->to.width == 2)
+		if(isfloat[p->to.etype] && FtoB(p->from.type)) {
+			switch(p->as) {
+			case AMOVSS:
+			case AMOVSD:
 				break;
-		case AMOVL:
-			if(p->to.width == 4)
-				break;
-		default:
+			default:
+				continue;
+			}
+		} else if(!isfloat[p->to.etype] && RtoB(p->from.type)) {
+			switch(p->as) {
+			case AMOVB:
+				if(p->to.width == 1)
+					break;
+			case AMOVW:
+				if(p->to.width == 2)
+					break;
+			case AMOVL:
+				if(p->to.width == 4)
+					break;
+			default:
+				continue;
+			}
+		} else
 			continue;
-		}
 		// p is a MOV reg, mem.
-		// and it is not a float.
 		p2 = p->link;
 		h = hash32to16(fnv1(p->to.sym));
 		if(counts[h] != 2) {
@@ -1872,7 +1879,9 @@ fixtemp(Prog *firstp)
 		}
 		switch(p2->as) {
 		case ALEAL:
-		case AFMOVL: 
+		case AFMOVD:
+		case AFMOVF:
+		case AFMOVL:
 		case AFMOVW:
 		case AFMOVV:
 			// funny

コアとなるコードの解説

fixtemp関数は、アセンブリ命令のリスト(Prog構造体のリンクリスト)を走査し、一時変数の最適化を試みます。

  1. 変更前の条件分岐: 変更前は、以下の条件のいずれかに合致する場合、現在の命令pに対する最適化をスキップしていました。

    • p->link == P: 命令がリストの最後である場合。
    • !RtoB(p->from.type): fromオペランドがレジスタからの移動ではない場合。
    • p->to.type != D_AUTO: toオペランドが自動変数(スタックに割り当てられる一時変数)ではない場合。
    • isfloat[p->to.etype]: toオペランドの型が浮動小数点型である場合。 特に最後の条件isfloat[p->to.etype]が重要で、これにより浮動小数点型の一時変数に対する最適化が完全にスキップされていました。
  2. 変更後の条件分岐: 変更後では、条件分岐がより詳細になりました。

    • if(p->link == P || p->to.type != D_AUTO): この条件は変更前とほぼ同じで、リストの最後か、自動変数ではない場合はスキップします。
    • if(isfloat[p->to.etype] && FtoB(p->from.type)):
      • isfloat[p->to.etype]: toオペランドが浮動小数点型であるか。
      • FtoB(p->from.type): fromオペランドがフレーム(スタック)からの移動であるか。
      • この両方が真の場合、つまり浮動小数点型の一時変数がスタックから移動される命令である場合に、内部のswitch文に入ります。
      • switch(p->as): 命令の種類をチェックします。
        • case AMOVSS: 単精度浮動小数点数の移動命令。
        • case AMOVSD: 倍精度浮動小数点数の移動命令。
        • これらのSSE2関連の移動命令であれば、breakして後続の最適化ロジックに進みます。
        • default: それ以外の命令であれば、continueして次の命令に移ります。
    • else if(!isfloat[p->to.etype] && RtoB(p->from.type)):
      • !isfloat[p->to.etype]: toオペランドが浮動小数点型ではないか。
      • RtoB(p->from.type): fromオペランドがレジスタからの移動であるか。
      • この両方が真の場合、つまり非浮動小数点型の一時変数がレジスタから移動される命令である場合に、内部のswitch文に入ります。
      • switch(p->as): 命令の種類をチェックします。
        • case AMOVB, case AMOVW, case AMOVL: それぞれ1バイト、2バイト、4バイトの移動命令。
        • これらの命令であれば、breakして後続の最適化ロジックに進みます。
        • default: それ以外の命令であれば、continueして次の命令に移ります。
    • else continue;: 上記のどの条件にも合致しない場合は、次の命令に移ります。

この変更により、fixtemp関数は浮動小数点型の一時変数に対しても、その移動命令がAMOVSSAMOVSDである場合に限り、最適化の対象とするようになりました。これにより、SSE2コードにおける一時変数の不必要なメモリへのスピルが減少し、レジスタの効率的な利用が促進されます。

また、AFMOVD, AFMOVFswitch(p2->as)に追加されています。これらも浮動小数点数関連の命令であり、後続の命令がこれらの浮動小数点移動命令である場合にも、一時変数の最適化を考慮するようになったことを示唆しています。

関連リンク

参考にした情報源リンク