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

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

このコミットは、Go言語の32ビットx86アーキテクチャ向けコンパイラであるcmd/8gにおける最適化を導入しています。具体的には、文字列やスライスの要素のアドレスを取得する操作(例: &s[i])において、不要なアドレス計算を削減し、生成される機械語コードの効率を向上させています。この変更により、LEAL (Load Effective Address) 命令の使用回数が減少し、複数のベンチマークでパフォーマンスの改善が見られます。

コミット

commit 2de064b63c6d0eb5cba5bd877da81c8100aaeb2a
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Oct 2 08:19:27 2012 +0200

    cmd/8g: do not take the address of string/slice for &s[i]
    
    A similar change was made in 6g recently.
    
    LEALs in cmd/go: 31440 before, 27867 after.
    
    benchmark                 old ns/op    new ns/op    delta
    BenchmarkBinaryTree17    7065794000   6723617000   -4.84%
    BenchmarkFannkuch11      7767395000   7477945000   -3.73%
    BenchmarkGobDecode         34708140     34857820   +0.43%
    BenchmarkGobEncode         10998780     10960060   -0.35%
    BenchmarkGzip            1603630000   1471052000   -8.27%
    BenchmarkGunzip           242573900    240650400   -0.79%
    BenchmarkJSONEncode       120842200    117966100   -2.38%
    BenchmarkJSONDecode       247254900    249103100   +0.75%
    BenchmarkMandelbrot200     29237330     29241790   +0.02%
    BenchmarkParse              8111320      8096865   -0.18%
    BenchmarkRevcomp         2595780000   2694153000   +3.79%
    BenchmarkTemplate         276679600    264497000   -4.40%
    
    benchmark                              old ns/op    new ns/op    delta
    BenchmarkAppendFloatDecimal                  429          416   -3.03%
    BenchmarkAppendFloat                         780          740   -5.13%
    BenchmarkAppendFloatExp                      746          700   -6.17%
    BenchmarkAppendFloatNegExp                   752          694   -7.71%
    BenchmarkAppendFloatBig                     1228         1108   -9.77%
    BenchmarkAppendFloat32Integer                457          416   -8.97%
    BenchmarkAppendFloat32ExactFraction          662          631   -4.68%
    BenchmarkAppendFloat32Point                  771          735   -4.67%
    BenchmarkAppendFloat32Exp                    722          672   -6.93%
    BenchmarkAppendFloat32NegExp                 724          659   -8.98%
    BenchmarkAppendFloat64Fixed1                 429          400   -6.76%
    BenchmarkAppendFloat64Fixed2                 463          442   -4.54%
    
    Update #1914.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/6574043

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

https://github.com/golang/go/commit/2de064b63c6d0eb5cba5bd877da81c8100aaeb2a

元コミット内容

このコミットは、Goコンパイラcmd/8gにおいて、文字列やスライスの要素のアドレスを取得する際のコード生成ロジックを改善するものです。具体的には、&s[i]のような操作において、コンパイラが不必要に文字列やスライス全体のベースアドレスを取得するのではなく、直接要素のアドレスを計算するように変更されています。これにより、生成されるアセンブリコードの効率が向上し、特にLEAL命令の使用回数が削減されます。

変更の背景

Go言語のコンパイラは、生成されるバイナリのパフォーマンスを最大化するために継続的に最適化が行われています。このコミットの背景には、&s[i]のような一般的な操作において、コンパイラが冗長なアドレス計算を行っているという認識がありました。特に、x86アーキテクチャのLEAL命令は、アドレス計算だけでなく汎用的な算術演算にも使用されますが、その使用が最適でない場合、パフォーマンスのボトルネックとなる可能性があります。

コミットメッセージに「A similar change was made in 6g recently.」とあるように、64ビットx86アーキテクチャ向けの6gコンパイラでも同様の最適化が先行して行われており、その成功を受けて8gにも適用されたと考えられます。これは、Goコンパイラ開発チームが、異なるアーキテクチャのコンパイラ間で共通の最適化戦略を適用し、全体的なコード生成品質を向上させようとしていることを示唆しています。

具体的なパフォーマンス指標としてLEALs in cmd/go: 31440 before, 27867 after.と示されているように、この最適化によってLEAL命令の生成数が約11%削減されており、これがベンチマーク結果に現れるパフォーマンス向上に直結しています。

前提知識の解説

Goコンパイラ (cmd/8g, cmd/6g)

Go言語のコンパイラは、ターゲットアーキテクチャごとに異なる実装を持っています。

  • cmd/8g: 32ビットx86 (Intel/AMD) アーキテクチャ向けのGoコンパイラです。Go 1.x系の時代には、8ggo buildコマンドの内部で32ビット環境向けにコードを生成する役割を担っていました。
  • cmd/6g: 64ビットx86 (Intel/AMD) アーキテクチャ向けのGoコンパイラです。現代のGo開発で最も一般的に使用されるコンパイラの一つです。

これらのコンパイラは、Goのソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語コードを生成します。コード生成の段階で、様々な最適化が適用されます。

&s[i] 操作

Go言語において、sがスライスまたは文字列である場合、s[i]はインデックスiにある要素を参照します。&s[i]は、その要素のメモリ上のアドレスを取得する操作です。例えば、byteスライスbに対して&b[0]は、スライスの最初の要素へのポインタを返します。

LEAL (Load Effective Address) 命令

LEALはx86アセンブリ言語の命令で、「Load Effective Address」の略です。この命令は、メモリからデータをロードするのではなく、オペランドで指定されたアドレスを計算し、その結果をレジスタに格納します。

例: LEAL (%EAX, %EBX, 4), %ECX これは、ECX = EAX + EBX * 4 という計算を行い、結果をECXレジスタに格納します。

LEALは、以下のような特徴からコンパイラ最適化の対象となりやすい命令です。

  1. アドレス計算: メモリ上の特定のアドレスを計算するために使用されます。
  2. 汎用的な算術演算: メモリへのアクセスを伴わないため、ADDMULといった算術命令の組み合わせよりも効率的に複数の算術演算(加算、乗算、シフト)を一度に行うことができます。例えば、x * 4 + yのような計算を効率的に実行できます。
  3. フラグへの影響なし: LEAL命令はCPUのフラグレジスタ(ゼロフラグ、キャリーフラグなど)を変更しません。これにより、後続の条件分岐などに影響を与えずにアドレス計算を行うことができます。

コンパイラは、コード内のアドレス計算パターンを認識し、可能な限りLEAL命令を使用して最適化を図ります。しかし、不必要なLEALの使用は、レジスタの割り当てやパイプラインの効率に悪影響を与える可能性があるため、その使用を最適化することは重要です。

ベンチマーク

Go言語には、標準でベンチマーク機能が組み込まれています。go test -bench=.コマンドを実行することで、コードのパフォーマンスを測定できます。ベンチマーク結果は通常、操作あたりのナノ秒(ns/op)で表示され、数値が小さいほど高速であることを示します。このコミットのベンチマーク結果は、変更前(old ns/op)と変更後(new ns/op)のパフォーマンスを比較し、delta(変化率)で改善度合いを示しています。負のデルタはパフォーマンスの向上を意味します。

技術的詳細

このコミットの技術的詳細は、src/cmd/8g/cgen.cファイル内のコード生成ロジックの変更に集約されます。

cgenindexからigenindexへの変更

まず、cgenindexという関数がigenindexにリネームされています。この関数は、配列のインデックスを処理し、そのインデックスに対応するアドレス可能なノード(メモリ上の位置を指す情報)を生成する役割を担っています。

変更前は、cgenindexが常にインデックスnに対してcgen(n, res)を呼び出し、結果をresに格納していました。cgenは汎用的なコード生成関数であり、nが既にアドレス可能である場合でも、冗長なコードを生成する可能性がありました。

変更後のigenindexでは、if(n->addable)という条件が追加されています。これは、入力ノードnが既にアドレス可能(つまり、その値がメモリ上の特定の位置に直接対応している)であるかどうかをチェックします。

  • もしnが既にアドレス可能であれば、*res = *n;として、resnの情報を直接コピーします。これにより、不必要なコード生成をスキップできます。
  • nがアドレス可能でない場合は、一時的な名前を割り当ててからcgen(n, res)を呼び出し、コードを生成します。

この変更は、インデックスが既にレジスタにロードされている場合など、不必要なメモリ操作やアドレス計算を避けるための最適化です。

agen関数における&s[i]の最適化

agen関数は、Goコンパイラにおけるアドレス生成の主要な関数です。この関数は、与えられたノードnのアドレスを計算し、その結果をresノードに格納します。&s[i]のような操作は、このagen関数内で処理されます。

主要な変更点は以下の通りです。

  1. agenr関数の削除: 以前はagenrというヘルパー関数が存在し、アドレスをレジスタに生成する役割を担っていました。このコミットではagenrが削除され、その機能がagen関数内に統合またはより効率的な方法で処理されるようになりました。これは、コードの簡素化と、より直接的なコード生成パスの実現に貢献しています。

  2. インデックス処理の改善:

    • 以前は、インデックスnrが定数でない場合、一時的なノードtmpを作成し、cgenindex(nr, &tmp)を呼び出してインデックスの値をtmpに格納していました。
    • 変更後では、igenindex(nr, &tmp)を呼び出すことで、インデックスが既にアドレス可能であればその情報を直接利用し、そうでなければ必要なコードを生成するように改善されています。これにより、インデックス値の取得がより効率的になります。
  3. ベースポインタの直接的なロード:

    • スライスや文字列の要素のアドレスを計算する際、以前はArray_arrayフィールド(データ配列の開始アドレス)を介してベースポインタを取得していました。
    • 変更後では、// Load base pointer in n3.というコメントが示すように、ベースポインタを直接n3(一時的なノード)にロードするロジックが明確化されています。特に、固定長配列の場合には、agen(&n3, &n2)のように、配列自体のベースアドレスを直接取得する処理が追加されています。
  4. LEAL命令の最適化された使用:

    • w(要素の幅)が1, 2, 4, 8バイトの場合、LEAL (n3)(n2*w), n3という形式のLEAL命令が生成されるようになりました。これは、ベースアドレスn3にインデックスn2と要素幅wを乗算したオフセットを加算し、その結果を再びn3に格納するという効率的なアドレス計算です。
    • 以前は、wが0でない場合に、OMUL(乗算)とOADD(加算)を個別に生成していた可能性があります。LEAL命令は、これらの操作を単一の命令で実行できるため、命令数の削減と実行効率の向上に寄与します。
    • LEAL命令のscaleフィールド(乗数)を直接設定することで、コンパイラがより最適化されたアドレス計算命令を生成できるようになります。

これらの変更により、&s[i]のような操作が、より少ない命令数で、かつより効率的なx86アセンブリ命令(特にLEAL)を使用してコンパイルされるようになります。これにより、実行時のパフォーマンスが向上します。

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

src/cmd/8g/cgen.c

--- a/src/cmd/8g/cgen.c
+++ b/src/cmd/8g/cgen.c
@@ -456,23 +456,30 @@ flt2:	// binary
 }
 
 /*
- * generate array index into res.
- * n might be any size; res is 32-bit.
+ * generate an addressable node in res, containing the value of n.
+ * n is an array index, and might be any size; res width is <= 32-bit.
  * returns Prog* to patch to panic call.
  */
 Prog*
-cgenindex(Node *n, Node *res)
+igenindex(Node *n, Node *res)
 {
 	Node tmp, lo, hi, zero;
 
 	if(!is64(n->type)) {
-		cgen(n, res);
+		if(n->addable) {
+			// nothing to do.
+			*res = *n;
+		} else {
+			tempname(res, types[TUINT32]);
+			cgen(n, res);
+		}
 		return nil;
 	}
 
 	tempname(&tmp, types[TINT64]);
 	cgen(n, &tmp);
 	split64(&tmp, &lo, &hi);
+	tempname(res, types[TUINT32]);
 	gmove(&lo, res);
 	if(debug['B']) {
 		splitclean();
@@ -492,7 +499,7 @@ void
 agen(Node *n, Node *res)
 {
 	Node *nl, *nr;
-	Node n1, n2, n3, n4, tmp;
+	Node n1, n2, n3, n4, tmp, nlen;
 	Type *t;
 	uint32 w;
 	uint64 v;
@@ -574,109 +581,117 @@ agen(Node *n, Node *res)
 		p2 = nil;  // to be patched to panicindex.
 		w = n->type->width;
 		if(nr->addable) {
-			if(!isconst(nr, CTINT))
-				tempname(&tmp, types[TINT32]);
+			// Generate &nl first, and move nr into register.
 			if(!isconst(nl, CTSTR))
-				agenr(nl, &n3, res);
+				igen(nl, &n3, res);
 			if(!isconst(nr, CTINT)) {
-				p2 = cgenindex(nr, &tmp);
+				p2 = igenindex(nr, &tmp);
 				regalloc(&n1, tmp.type, N);
 				gmove(&tmp, &n1);
 			}
 		} else if(nl->addable) {
+			// Generate nr first, and move &nl into register.
 			if(!isconst(nr, CTINT)) {
-				tempname(&tmp, types[TINT32]);
-				p2 = cgenindex(nr, &tmp);
+				p2 = igenindex(nr, &tmp);
 				regalloc(&n1, tmp.type, N);
 				gmove(&tmp, &n1);
 			}
-			if(!isconst(nl, CTSTR)) {
-				regalloc(&n3, types[tptr], res);
-				agen(nl, &n3);
-			}
+			if(!isconst(nl, CTSTR))
+				igen(nl, &n3, res);
 		} else {
-			tempname(&tmp, types[TINT32]);
-			p2 = cgenindex(nr, &tmp);
+			p2 = igenindex(nr, &tmp);
 			nr = &tmp;
 			if(!isconst(nl, CTSTR))
-				agenr(nl, &n3, res);
+				igen(nl, &n3, res);
 			regalloc(&n1, tmp.type, N);
 			gins(optoas(OAS, tmp.type), &tmp, &n1);
 		}
 
-		// &a is in &n3 (allocated in res)
-		// i is in &n1 (if not constant)
+		// For fixed array we really want the pointer in n3.
+		if(isfixedarray(nl->type)) {
+			regalloc(&n2, types[tptr], &n3);
+			agen(&n3, &n2);
+			regfree(&n3);
+			n3 = n2;
+		}
+
+		// &a[0] is in n3 (allocated in res)
+		// i is in n1 (if not constant)
+		// len(a) is in nlen (if needed)
 		// w is width
 
 		// explicit check for nil if array is large enough
 		// that we might derive too big a pointer.
 		if(isfixedarray(nl->type) && nl->type->width >= unmappedzero) {
-			regalloc(&n4, types[tptr], &n3);
-			gmove(&n3, &n4);
+			n4 = n3;
 			n4.op = OINDREG;
 			n4.type = types[TUINT8];
 			n4.xoffset = 0;
 			gins(ATESTB, nodintconst(0), &n4);
-			regfree(&n4);
 		}
 
 		// constant index
 		if(isconst(nr, CTINT)) {
 			if(isconst(nl, CTSTR))
-				fatal("constant string constant index");
+				fatal("constant string constant index");  // front end should handle
 			v = mpgetfix(nr->val.u.xval);
 			if(isslice(nl->type) || nl->type->etype == TSTRING) {
 				if(!debug['B'] && !n->bounded) {
-					n1 = n3;
-					n1.op = OINDREG;
-					n1.type = types[tptr];
-					n1.xoffset = Array_nel;
+					nlen = n3;
+					nlen.type = types[TUINT32];
+					nlen.xoffset += Array_nel;
 					nodconst(&n2, types[TUINT32], v);
-					gins(optoas(OCMP, types[TUINT32]), &n1, &n2);
+					gins(optoas(OCMP, types[TUINT32]), &nlen, &n2);
 					p1 = gbranch(optoas(OGT, types[TUINT32]), T, +1);
 					ginscall(panicindex, -1);
 					patch(p1, pc);
 				}
-
-				n1 = n3;
-				n1.op = OINDREG;
-				n1.type = types[tptr];
-				n1.xoffset = Array_array;
-				gmove(&n1, &n3);
 			}
 
+			// Load base pointer in n2 = n3.
+			regalloc(&n2, types[tptr], &n3);
+			n3.type = types[tptr];
+			n3.xoffset += Array_array;
+			gmove(&n3, &n2);
+			regfree(&n3);
 			if (v*w != 0) {
-				nodconst(&n2, types[tptr], v*w);
-				gins(optoas(OADD, types[tptr]), &n2, &n3);
+				nodconst(&n1, types[tptr], v*w);
+				gins(optoas(OADD, types[tptr]), &n1, &n2);
 			}
-			gmove(&n3, res);
-			regfree(&n3);
+			gmove(&n2, res);
+			regfree(&n2);
 			break;
 		}
 
-		regalloc(&n2, types[TINT32], &n1);			// i
+		// i is in register n1, extend to 32 bits.
+		t = types[TUINT32];
+		if(issigned[n1.type->etype])
+			t = types[TINT32];
+
+		regalloc(&n2, t, &n1);			// i
 		gmove(&n1, &n2);
 		regfree(&n1);
 
 		if(!debug['B'] && !n->bounded) {
 			// check bounds
 			if(isconst(nl, CTSTR))
-				nodconst(&n1, types[TUINT32], nl->val.u.sval->len);
+				nodconst(&nlen, t, 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;
+				nlen = n3;
+				nlen.type = t;
+				nlen.xoffset += Array_nel;
 			} else
-				nodconst(&n1, types[TUINT32], nl->type->bound);
-			gins(optoas(OCMP, types[TUINT32]), &n2, &n1);
-			p1 = gbranch(optoas(OLT, types[TUINT32]), T, +1);
+				nodconst(&nlen, t, nl->type->bound);
+			gins(optoas(OCMP, t), &n2, &nlen);
+			p1 = gbranch(optoas(OLT, t), T, +1);
 			if(p2)
 				patch(p2, pc);
 			ginscall(panicindex, -1);
 			patch(p1, pc);
 		}
-		
+
 		if(isconst(nl, CTSTR)) {
 			regalloc(&n3, types[tptr], res);
 			p1 = gins(ALEAL, N, &n3);
@@ -686,24 +701,27 @@ agen(Node *n, Node *res)
 			goto indexdone;
 		}
 
+		// Load base pointer in n3.
+		regalloc(&tmp, types[tptr], &n3);
 		if(isslice(nl->type) || nl->type->etype == TSTRING) {
-			n1 = n3;
-			n1.op = OINDREG;
-			n1.type = types[tptr];
-			n1.xoffset = Array_array;
-			gmove(&n1, &n3);
+			n3.type = types[tptr];
+			n3.xoffset += Array_array;
+			gmove(&n3, &tmp);
 		}
+		regfree(&n3);
+		n3 = tmp;
 
 		if(w == 0) {
 			// nothing to do
 		} else if(w == 1 || w == 2 || w == 4 || w == 8) {
+			// LEAL (n3)(n2*w), n3
 			p1 = gins(ALEAL, &n2, &n3);
 			p1->from.scale = w;
 			p1->from.index = p1->from.type;
 			p1->from.type = p1->to.type + D_INDIR;
 		} else {
-			nodconst(&n1, types[TUINT32], w);
-			gins(optoas(OMUL, types[TUINT32]), &n1, &n2);
+			nodconst(&tmp, types[TUINT32], w);
+			gins(optoas(OMUL, types[TUINT32]), &tmp, &n2);
 			gins(optoas(OADD, types[tptr]), &n2, &n3);
 		}
 
@@ -861,23 +879,6 @@ igen(Node *n, Node *a, Node *res)
 	a->type = n->type;
 }
 
-/*
- * generate:
- *	newreg = &n;
- *
- * caller must regfree(a).
- */
-void
-agenr(Node *n, Node *a, Node *res)
-{
-	Node n1;
-
-	tempname(&n1, types[tptr]);
-	agen(n, &n1);
-	regalloc(a, types[tptr], res);
-	gmove(&n1, a);
-}
-
 /*
  * branch gen
  *	if(n == true) goto to;

src/cmd/8g/gg.h

--- a/src/cmd/8g/gg.h
+++ b/src/cmd/8g/gg.h
@@ -95,7 +95,6 @@ void	ginscall(Node*, int);
  * cgen.c
  */
 void	agen(Node*, Node*);
-void	agenr(Node *n, Node *a, Node *res);
 void	igen(Node*, Node*, Node*);
 vlong	fieldoffset(Type*, Node*);
 void	sgen(Node*, Node*, int64);

コアとなるコードの解説

igenindex関数の変更

  • 目的: この関数は、配列やスライスのインデックスnを受け取り、そのインデックスに対応するアドレス可能なノードresを生成します。
  • 変更点:
    • 関数名がcgenindexからigenindexに変更されました。
    • if(!is64(n->type))ブロック内で、n->addableという新しいチェックが追加されました。
      • n->addableが真の場合(つまり、インデックスnが既にメモリ上のアドレスに直接対応している場合)、*res = *n;として、nの情報をresに直接コピーします。これにより、不必要なコード生成(cgenの呼び出し)を回避し、効率を向上させます。
      • n->addableが偽の場合、以前と同様に一時的なノードを作成し、cgen(n, res)を呼び出してコードを生成します。
    • 64ビットインデックスを32ビットに変換する際も、tempname(res, types[TUINT32]);が追加され、resが適切に初期化されるようになりました。

agen関数の変更

  • 目的: agen関数は、Goコンパイラにおけるアドレス生成の主要な関数であり、&s[i]のような操作を処理します。
  • agenrの削除: agenr関数は、アドレスをレジスタに生成するためのヘルパー関数でしたが、このコミットで完全に削除されました。これは、agen関数自体のロジックが改善され、agenrの機能が不要になったか、より効率的な方法で統合されたことを意味します。
  • インデックス処理の改善:
    • nr->addableまたはnl->addableの条件分岐内で、インデックスnrの処理にigenindexが使用されるようになりました。これにより、インデックスが既にアドレス可能であれば、その情報を直接利用し、冗長なコード生成を避けます。
    • // For fixed array we really want the pointer in n3.というコメントが追加され、固定長配列の場合にベースポインタをn3に適切にロードするロジックが追加されました。これは、agen(&n3, &n2)を呼び出して、配列自体のベースアドレスを取得し、それをn3に設定するものです。
  • ベースポインタのロードの明確化:
    • スライスや文字列の要素のアドレスを計算する際、// Load base pointer in n3.というコメントが追加され、ベースポインタをn3にロードする処理がより明確になりました。具体的には、Array_arrayフィールド(データ配列の開始アドレス)を介してベースポインタを取得し、それを一時的なノードtmpに格納し、最終的にn3に設定します。
  • LEAL命令の最適化:
    • 要素の幅wが1, 2, 4, 8バイトの場合、gins(ALEAL, &n2, &n3);という行が追加され、LEAL命令が生成されるようになりました。
    • このLEAL命令は、p1->from.scale = w;のように、scaleフィールドに要素の幅wを設定することで、LEAL (n3)(n2*w), n3という形式の命令を生成します。これは、ベースアドレスn3に、インデックスn2と要素幅wを乗算したオフセットを加算し、その結果をn3に格納するという、非常に効率的なアドレス計算です。
    • 以前は、wが1, 2, 4, 8バイトの場合でも、OMULOADDを個別に生成していた可能性がありますが、LEALを使用することで、これらの操作を単一の命令で実行できるようになり、命令数の削減と実行効率の向上に貢献します。

これらの変更は、コンパイラが&s[i]のような操作を処理する際に、より少ない中間ステップで、かつより効率的なx86アセンブリ命令(特にLEAL)を生成するように設計されています。これにより、生成されるバイナリのサイズが小さくなり、実行速度が向上します。

関連リンク

参考にした情報源リンク