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

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

このコミットは、Go言語のamd64アーキテクチャ向けコンパイラ(cmd/6g)における浮動小数点レジスタの使用効率と最適化に関する改善を目的としています。具体的には、利用可能な16個の浮動小数点レジスタをすべて活用し、浮動小数点値の移動(float moves)を最適化することで、生成されるコードのパフォーマンス向上を図っています。

コミット

commit 57ad05db1540fd949917708a7551866a2c9e2a47
Author: Russ Cox <rsc@golang.org>
Date:   Fri Sep 21 13:39:09 2012 -0400

    cmd/6g: use all 16 float registers, optimize float moves
    
    Fixes #2446.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6557044

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

https://github.com/golang/go/commit/57ad05db1540fd949917708a7551866a2c9e2a47

元コミット内容

cmd/6g: use all 16 float registers, optimize float moves

Fixes #2446.

R=ken2
CC=golang-dev
https://golang.org/cl/6557044

変更の背景

この変更の背景には、Goコンパイラがamd64アーキテクチャの浮動小数点レジスタを十分に活用できていなかったという問題があります。amd64(x86-64)アーキテクチャには、SSE/SSE2命令セットによって提供される16個のXMMレジスタ(XMM0からXMM15)が存在し、これらは浮動小数点演算に利用されます。しかし、Goコンパイラの以前の実装では、これらのレジスタのうち一部(おそらくXMM0からXMM7までの8個)しか利用しておらず、残りのレジスタが未使用の状態でした。

レジスタはCPUがデータを高速に処理するために使用する非常に高速な記憶領域です。コンパイラが利用可能なレジスタを最大限に活用することは、メモリへのアクセスを減らし、CPUのパイプラインを効率的に利用することで、プログラムの実行速度を向上させる上で極めて重要です。特に浮動小数点演算は科学技術計算やグラフィックス処理などで頻繁に用いられるため、その性能向上は多くのアプリケーションに影響を与えます。

このコミットは、Go issue #2446を修正するものであり、この問題がコミュニティによって認識され、改善が求められていたことを示唆しています。コンパイラがより多くのレジスタを利用できるようになることで、レジスタスピル(レジスタの内容をメモリに退避させる処理)が減少し、結果として生成される機械語コードの効率が向上します。

前提知識の解説

1. cmd/6g

cmd/6gは、Go言語のツールチェインの一部であり、amd64(x86-64)アーキテクチャ向けのGoコンパイラを指します。Go言語のソースコードをこのアーキテクチャで実行可能なバイナリに変換する役割を担っています。Goのコンパイラは、特定のアーキテクチャごとにcmd/<arch>gという命名規則で存在します(例: cmd/8g386向け、cmd/5gARM向けなど)。

2. 浮動小数点レジスタ (XMMレジスタ)

amd64アーキテクチャでは、浮動小数点演算やSIMD(Single Instruction, Multiple Data)演算のために、XMMレジスタと呼ばれる特別なレジスタセットが提供されています。

  • XMMレジスタ: 128ビット幅のレジスタで、XMM0からXMM15までの合計16個が存在します。これらは単精度浮動小数点数(float32)を4つ、または倍精度浮動小数点数(float64)を2つ格納できます。
  • SSE/SSE2: Streaming SIMD Extensions (SSE) および SSE2 は、Intelによって導入された命令セット拡張であり、XMMレジスタを利用して浮動小数点演算やSIMD演算を高速化します。現代のほとんどのamd64プロセッサはこれらの命令セットをサポートしています。

3. レジスタ割り当て (Register Allocation)

コンパイラの重要なフェーズの一つに「レジスタ割り当て」があります。これは、プログラムの変数や中間結果をCPUのレジスタにどのように割り当てるかを決定するプロセスです。

  • 目的: メモリへのアクセスはCPUレジスタへのアクセスよりもはるかに遅いため、可能な限り多くの値をレジスタに保持することで、プログラムの実行速度を向上させます。
  • 課題: CPUのレジスタ数は限られているため、コンパイラはどの値をレジスタに保持し、どの値をメモリに退避させるか(レジスタスピル)を賢く決定する必要があります。利用可能なレジスタが多いほど、レジスタスピルを減らし、より効率的なコードを生成できます。

4. コピー伝播 (Copy Propagation)

コピー伝播は、コンパイラの最適化手法の一つです。これは、ある変数が別の変数のコピーである場合、そのコピーされた変数の使用箇所を元の変数で置き換えることで、冗長なデータ移動を排除しようとするものです。 例:

a = b
c = a + 1

コピー伝播を適用すると、c = b + 1 となり、a の使用が不要になる場合があります。これにより、レジスタ間の移動命令などを削減し、コードを効率化できます。

技術的詳細

このコミットは、主に以下の3つの側面でamd64コンパイラの浮動小数点処理を改善しています。

1. 浮動小数点レジスタの利用範囲拡大 (src/cmd/6g/gsubr.c, src/cmd/6l/6.out.h)

  • src/cmd/6l/6.out.h: このファイルは、Goリンカ(cmd/6l)が使用するアセンブラ命令やレジスタの定義を含んでいます。変更点として、D_X8からD_X15までの新しい浮動小数点レジスタが列挙型enumに追加されています。これにより、コンパイラとリンカがXMM0からXMM15までの全16個のXMMレジスタを認識し、利用できるようになります。
    • 以前はD_X0からD_X7までしか定義されていなかったため、8個のレジスタしか利用できませんでした。
  • src/cmd/6g/gsubr.c: このファイルは、Goコンパイラのバックエンドにおける汎用サブルーチンを含んでいます。
    • ginit()関数とgclean()関数内のレジスタ初期化およびクリーンアップループが、D_X7までではなくD_X15までを対象とするように変更されています。これは、コンパイラがレジスタ割り当てを行う際に、全16個の浮動小数点レジスタを考慮に入れることを意味します。
    • regalloc()関数(レジスタ割り当てを行う関数)も同様に、浮動小数点レジスタの検索範囲をD_X0からD_X15に拡張しています。これにより、コンパイラは利用可能なすべての浮動小数点レジスタの中から空いているものを見つけて割り当てることができるようになります。

2. 浮動小数点移動命令の最適化 (src/cmd/6g/peep.c)

  • src/cmd/6g/peep.c: このファイルは、Goコンパイラの「ピーフホール最適化(Peephole Optimization)」を担当しています。ピーフホール最適化は、生成されたアセンブリコードの小さな「窓(peephole)」を見て、より効率的な命令シーケンスに置き換える最適化手法です。
    • subprop()関数は、コピー伝播最適化の一部として機能します。この関数は、特定の命令パターンを検出し、冗長なデータ移動を排除しようとします。
    • 変更点として、AMOVSS(単精度浮動小数点移動)とAMOVSD(倍精度浮動小数点移動)命令が、コピー伝播の対象として追加されています。これにより、浮動小数点値のレジスタ間移動が冗長である場合に、コンパイラがそれを検出し、最適化できるようになります。例えば、MOVSD XMM0, XMM1 の後に MOVSD XMM1, XMM0 のような冗長な移動がある場合、これらを削除したり、より効率的なシーケンスに置き換えたりすることが可能になります。
    • また、デバッグ出力(debug['P'] && debug['v'])が追加されており、最適化プロセスの可視性が向上しています。これは、最適化がどのように適用されているかを開発者が追跡しやすくするためのものです。

3. レジスタ番号変換ロジックの修正 (src/cmd/6g/reg.c)

  • src/cmd/6g/reg.c: このファイルは、レジスタ関連のユーティリティ関数を含んでいます。
    • BtoF()関数は、おそらくビットマスクから浮動小数点レジスタのインデックスを計算する関数です。
    • 変更点として、ビットマスクの処理が0xFF0000Lから0xFFFF0000Lに変更されています。これは、レジスタのインデックスを正しく抽出するために、より広いビット範囲を考慮する必要があることを示唆しています。具体的には、以前は8ビット(0xFF)のシフトされた値を見ていたのに対し、新しい変更では16ビット(0xFFFF)のシフトされた値を見るようになっています。これは、レジスタの識別子が拡張された(XMM0-XMM15)ことに対応するための修正と考えられます。

これらの変更は全体として、Goコンパイラがamd64アーキテクチャの浮動小数点ハードウェアをより効果的に利用し、浮動小数点演算を含むプログラムのパフォーマンスを向上させることを目的としています。

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

src/cmd/6g/gsubr.c

--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -271,7 +271,7 @@ ginit(void)
 		reg[i] = 1;
 	for(i=D_AX; i<=D_R15; i++)
 		reg[i] = 0;
-	for(i=D_X0; i<=D_X7; i++)
+	for(i=D_X0; i<=D_X15; i++)
 		reg[i] = 0;
 
 	for(i=0; i<nelem(resvd); i++)
@@ -289,7 +289,7 @@ gclean(void)
 	for(i=D_AX; i<=D_R15; i++)
 		if(reg[i])
 			yyerror("reg %R left allocated\\n", i);
-	for(i=D_X0; i<=D_X7; i++)
+	for(i=D_X0; i<=D_X15; i++)
 		if(reg[i])
 			yyerror("reg %R left allocated\\n", i);
 }
@@ -359,10 +359,10 @@ regalloc(Node *n, Type *t, Node *o)
 	case TFLOAT64:
 		if(o != N && o->op == OREGISTER) {
 			i = o->val.u.reg;
-			if(i >= D_X0 && i <= D_X7)
+			if(i >= D_X0 && i <= D_X15)
 				goto out;
 		}
-		for(i=D_X0; i<=D_X7; i++)
+		for(i=D_X0; i<=D_X15; i++)
 			if(reg[i] == 0)
 				goto out;
 		fatal("out of floating registers");

src/cmd/6g/peep.c

--- a/src/cmd/6g/peep.c
+++ b/src/cmd/6g/peep.c
@@ -627,19 +627,34 @@ subprop(Reg *r0)
 	Reg *r;
 	int t;
 
+	if(debug['P'] && debug['v'])
+		print("subprop %P\\n", r0->prog);
 	p = r0->prog;
 	v1 = &p->from;
-	if(!regtyp(v1))
+	if(!regtyp(v1)) {
+		if(debug['P'] && debug['v'])
+			print("\tnot regtype %D; return 0\\n", v1);
 		return 0;
+	}
 	v2 = &p->to;
-	if(!regtyp(v2))
+	if(!regtyp(v2)) {
+		if(debug['P'] && debug['v'])
+			print("\tnot regtype %D; return 0\\n", v2);
 		return 0;
+	}
 	for(r=uniqp(r0); r!=R; r=uniqp(r)) {
-		if(uniqs(r) == R)
+		if(debug['P'] && debug['v'])
+			print("\t? %P\\n", r->prog);
+		if(uniqs(r) == R) {
+			if(debug['P'] && debug['v'])
+				print("\tno unique successor\\n");
 			break;
+		}
 		p = r->prog;
 		switch(p->as) {
 		case ACALL:
+			if(debug['P'] && debug['v'])
+				print("\tfound %P; return 0\\n", p);
 			return 0;
 
 		case AIMULL:
@@ -710,21 +725,33 @@ subprop(Reg *r0)
 		case AMOVSB:
 		case AMOVSL:
 		case AMOVSQ:
+			if(debug['P'] && debug['v'])
+				print("\tfound %P; return 0\\n", p);
 			return 0;
 
 		case AMOVL:
 		case AMOVQ:
+		case AMOVSS:
+		case AMOVSD:
 			if(p->to.type == v1->type)
 				goto gotit;
 			break;
 		}
 		if(copyau(&p->from, v2) ||
-		   copyau(&p->to, v2))
+		   copyau(&p->to, v2)) {
+		   	if(debug['P'] && debug['v'])
+		   		print("\tcopyau %D failed\\n", v2);
 			break;
+		}
 		if(copysub(&p->from, v1, v2, 0) ||
-		   copysub(&p->to, v1, v2, 0))
+		   copysub(&p->to, v1, v2, 0)) {
+		   	if(debug['P'] && debug['v'])
+		   		print("\tcopysub failed\\n");
 			break;
+		}
 	}
+	if(debug['P'] && debug['v'])
+		print("\tran off end; return 0\\n", p);
 	return 0;
 
 gotit:
@@ -769,6 +796,8 @@ copyprop(Reg *r0)
 	Adr *v1, *v2;
 	Reg *r;
 
+	if(debug['P'] && debug['v'])
+		print("copyprop %P\\n", r0->prog);
 	p = r0->prog;
 	v1 = &p->from;
 	v2 = &p->to;
@@ -1180,13 +1209,22 @@ int
 copyau(Adr *a, Adr *v)
 {
 
-	if(copyas(a, v))
+	if(copyas(a, v)) {
+		if(debug['P'] && debug['v'])
+			print("\tcopyau: copyas returned 1\\n");
 		return 1;
+	}
 	if(regtyp(v)) {
-		if(a->type-D_INDIR == v->type)
+		if(a->type-D_INDIR == v->type) {
+			if(debug['P'] && debug['v'])
+				print("\tcopyau: found indir use - return 1\\n");
 			return 1;
-		if(a->index == v->type)
+		}
+		if(a->index == v->type) {
+			if(debug['P'] && debug['v'])
+				print("\tcopyau: found index use - return 1\\n");
 			return 1;
+		}
 	}
 	return 0;
 }

src/cmd/6g/reg.c

--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -1601,7 +1601,7 @@ int
 BtoF(int32 b)
 {
 
-	b &= 0xFF0000L;
+	b &= 0xFFFF0000L;
 	if(b == 0)
 		return 0;
 	return bitno(b) - 16 + FREGMIN;

src/cmd/6l/6.out.h

--- a/src/cmd/6l/6.out.h
+++ b/src/cmd/6l/6.out.h
@@ -805,6 +805,14 @@ enum
 	D_X5,
 	D_X6,
 	D_X7,
+	D_X8,
+	D_X9,
+	D_X10,
+	D_X11,
+	D_X12,
+	D_X13,
+	D_X14,
+	D_X15,
 
 	D_CS		= 68,
 	D_SS,

コアとなるコードの解説

src/cmd/6g/gsubr.c の変更

  • レジスタ範囲の拡張: ginit()gclean()関数内のループ、およびregalloc()関数内のレジスタ検索ループが、D_X7からD_X15に拡張されています。これは、Goコンパイラが浮動小数点レジスタを管理する際に、XMM0からXMM15までの全16個のレジスタを対象とするようになったことを意味します。これにより、コンパイラはより多くのレジスタを自由に利用できるようになり、レジスタスピルの発生を抑え、コードの効率を向上させることが期待されます。

src/cmd/6g/peep.c の変更

  • 浮動小数点移動命令の最適化対象追加: subprop()関数において、AMOVSS(単精度浮動小数点移動)とAMOVSD(倍精度浮動小数点移動)命令が、コピー伝播最適化の対象として追加されました。これにより、コンパイラは浮動小数点値の冗長な移動を検出し、それらを削除または最適化することで、生成されるアセンブリコードのサイズと実行時間を削減できます。
  • デバッグ出力の追加: debug['P'] && debug['v']という条件付きのprint文が多数追加されています。これは、コンパイラの最適化フェーズ(特にコピー伝播)の動作を詳細にトレースするためのものです。開発者が最適化の適用状況やその効果をデバッグする際に役立ちます。

src/cmd/6g/reg.c の変更

  • ビットマスクの修正: BtoF()関数内のビットマスクが0xFF0000Lから0xFFFF0000Lに変更されました。これは、浮動小数点レジスタの識別子(インデックス)が拡張されたことに対応するための修正です。以前は8ビットの範囲でレジスタを識別していたのに対し、新しい変更では16ビットの範囲で識別することで、XMM0からXMM15までの全レジスタを正しく処理できるようになります。

src/cmd/6l/6.out.h の変更

  • 新しいレジスタ定義の追加: enumD_X8からD_X15までの新しい浮動小数点レジスタの定義が追加されました。これは、Goのツールチェイン全体(コンパイラ、アセンブラ、リンカ)が、amd64アーキテクチャの全16個のXMMレジスタを正式に認識し、利用するための基盤となります。

これらの変更は相互に関連しており、Goコンパイラがamd64アーキテクチャの浮動小数点ハードウェア機能を最大限に活用し、より高性能なコードを生成するための重要なステップです。

関連リンク

参考にした情報源リンク

  • x86-64 (amd64) アーキテクチャのレジスタセットに関する情報 (例: Wikipedia, Intel/AMDの公式ドキュメント)
  • コンパイラの最適化(レジスタ割り当て、コピー伝播、ピーフホール最適化)に関する一般的な情報 (例: コンパイラ設計の教科書、オンラインのコンピュータサイエンスリソース)
  • Go言語のコンパイラ内部構造に関する情報 (Goのソースコード、Goのブログ記事、Goの設計ドキュメントなど)
  • SSE/SSE2命令セットに関する情報 (Intelのプログラマーズマニュアルなど)