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

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

このコミットは、Goコンパイラ(5c, 5g)とリンカ(5l)における8ビットおよび16ビット整数値のレジスタ間移動命令のセマンティクスを明確化し、関連する最適化を改善するものです。具体的には、MOVBMOVHを単純な移動命令として扱い、符号拡張(sign-extension)やゼロ拡張(zero-extension)のセマンティクスをMOVBS/MOVBUMOVHS/MOVHUといった新しい擬似命令に分離することで、コンパイラのコード生成と最適化の精度を高めています。これにより、特に短い整数型に対する算術演算のパフォーマンスが向上しています。

コミット

  • コミットハッシュ: 357f73369510e3ef37e2a473a7fd9034b1ddeeed
  • Author: Rémy Oudompheng oudomphe@phare.normalesup.org
  • Date: Fri Aug 9 06:43:17 2013 +0200

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

https://github.com/golang/go/commit/357f73369510e3ef37e2a473a7fd9034b1ddeeed

元コミット内容

commit 357f73369510e3ef37e2a473a7fd9034b1ddeeed
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Aug 9 06:43:17 2013 +0200

    cmd/5c, cmd/5g, cmd/5l: turn MOVB, MOVH into plain moves, optimize short arithmetic.
    
    Pseudo-instructions MOVBS and MOVHS are used to clarify
    the semantics of short integers vs. registers:
     * 8-bit and 16-bit values in registers are assumed to always
       be zero-extended or sign-extended depending on their type.
     * MOVB is truncation or move of an already extended value
       between registers.
     * MOVBU enforces zero-extension at the destination (register).
     * MOVBS enforces sign-extension at the destination (register).
    And similarly for MOVH/MOVS/MOVHU.
    
    The linker is adapted to assemble MOVB and MOVH to an ordinary
    mov. Also a peephole pass in 5g that aims at eliminating
    redundant zero/sign extensions is improved.
    
    encoding/binary:
    benchmark                              old ns/op    new ns/op    delta
    BenchmarkReadSlice1000Int32s              220387       217185   -1.45%
    BenchmarkReadStruct                        12839        12910   +0.55%
    BenchmarkReadInts                           5692         5534   -2.78%
    BenchmarkWriteInts                          6137         6016   -1.97%
    BenchmarkPutUvarint32                        257          241   -6.23%
    BenchmarkPutUvarint64                        812          754   -7.14%
    benchmark                               old MB/s     new MB/s  speedup
    BenchmarkReadSlice1000Int32s               18.15        18.42    1.01x
    BenchmarkReadStruct                         5.45         5.42    0.99x
    BenchmarkReadInts                           5.27         5.42    1.03x
    BenchmarkWriteInts                          4.89         4.99    1.02x
    BenchmarkPutUvarint32                      15.56        16.57    1.06x
    BenchmarkPutUvarint64                       9.85        10.60    1.08x
    
    crypto/des:
    benchmark                              old ns/op    new ns/op    delta
    BenchmarkEncrypt                            7002         5169  -26.18%
    BenchmarkDecrypt                            7015         5195  -25.94%
    benchmark                               old MB/s     new MB/s  speedup
    BenchmarkEncrypt                            1.14         1.55    1.36x
    BenchmarkDecrypt                            1.14         1.54    1.35x
    
    strconv:
    benchmark                              old ns/op    new ns/op    delta
    BenchmarkAtof64Decimal                       457          385  -15.75%
    BenchmarkAtof64Float                         574          479  -16.55%
    BenchmarkAtof64FloatExp                     1035          906  -12.46%
    BenchmarkAtof64Big                          1793         1457  -18.74%
    BenchmarkAtof64RandomBits                   2267         2066   -8.87%
    BenchmarkAtof64RandomFloats                 1416         1194  -15.68%
    BenchmarkAtof32Decimal                       451          379  -15.96%
    BenchmarkAtof32Float                         547          435  -20.48%
    BenchmarkAtof32FloatExp                     1095          986   -9.95%
    BenchmarkAtof32Random                       1154         1006  -12.82%
    BenchmarkAtoi                               1415         1380   -2.47%
    BenchmarkAtoiNeg                            1414         1401   -0.92%
    BenchmarkAtoi64                             1744         1671   -4.19%
    BenchmarkAtoi64Neg                          1737         1662   -4.32%
    
    Fixes #1837.
    
    R=rsc, dave, bradfitz
    CC=golang-dev
    https://golang.org/cl/12424043

変更の背景

このコミットは、Goコンパイラとリンカにおける8ビット(byte, int8, uint8)および16ビット(int16, uint16)の短い整数型を扱う際のセマンティクスの曖昧さを解消し、それによって生じる非効率性を改善するために導入されました。

Go言語では、レジスタは通常、プロセッサのワードサイズ(例えば32ビットや64ビット)で扱われます。しかし、Goの型システムには8ビットや16ビットの整数型が存在します。これらの短い整数値をレジスタにロードしたり、レジスタ間で移動させたりする際に、残りのビット(上位ビット)をどのように扱うか(ゼロで埋めるか、符号ビットで埋めるか)が問題となります。

従来のGoコンパイラでは、MOVB(バイト移動)やMOVH(ハーフワード移動)といった命令が、文脈によってゼロ拡張または符号拡張を暗黙的に行うことがありました。この暗黙的な挙動は、コンパイラが生成するコードの予測可能性を低下させ、冗長な拡張命令の生成につながることがありました。特に、短い整数型に対する算術演算(加算、減算、乗算など)の後、結果がレジスタのフルワードに格納されるため、その後の操作で再度適切な拡張が必要になる場合がありました。

この問題は、Go issue #1837 "cmd/5g: short arithmetic is slow" で報告されており、短い整数型に対する算術演算が期待よりも遅いというパフォーマンス上の懸念が示されていました。このコミットは、このパフォーマンス問題を解決し、コンパイラがより効率的で正確なコードを生成できるようにすることを目的としています。

前提知識の解説

1. Goコンパイラとリンカ (5c, 5g, 5l)

Go言語のツールチェインは、特定のアーキテクチャ向けに設計されたコンパイラとリンカを含んでいます。このコミットで言及されている5c, 5g, 5lは、それぞれARMアーキテクチャ(Goの内部ではGOARCH=armまたはGOARCH=arm64に対応)向けのCコンパイラ、Goコンパイラ、リンカを指します。

  • 5c: Goのランタイムや標準ライブラリの一部でC言語で書かれた部分をコンパイルするCコンパイラ。
  • 5g: Go言語のソースコードをアセンブリコードにコンパイルするGoコンパイラ。
  • 5l: コンパイルされたオブジェクトファイルをリンクして実行可能ファイルを生成するリンカ。

これらのツールは、Goプログラムがターゲットアーキテクチャ上で効率的に動作するための機械語を生成する役割を担っています。

2. 擬似命令 (Pseudo-instructions)

擬似命令とは、アセンブラが最終的な機械語に変換する際に、一つ以上の実際のCPU命令に展開される抽象的な命令のことです。プログラマにとっては、より高レベルなセマンティクスで操作を記述できるため、コードの可読性と記述性が向上します。

このコミットでは、MOVBS, MOVBU, MOVHS, MOVHUといった新しい擬似命令が導入されています。これらは、レジスタへの値のロードや移動の際に、明示的に符号拡張またはゼロ拡張を行うことを指示します。

  • MOVB: バイト(8ビット)の移動。このコミット後は、単純な移動として扱われ、拡張は行わない。
  • MOVH: ハーフワード(16ビット)の移動。このコミット後は、単純な移動として扱われ、拡張は行わない。
  • MOVBS: バイト値をレジスタにロードする際に、符号拡張を行う。
  • MOVBU: バイト値をレジスタにロードする際に、ゼロ拡張を行う。
  • MOVHS: ハーフワード値をレジスタにロードする際に、符号拡張を行う。
  • MOVHU: ハーフワード値をレジスタにロードする際に、ゼロ拡張を行う。

3. ゼロ拡張 (Zero-extension) と 符号拡張 (Sign-extension)

コンピュータアーキテクチャにおいて、短いビット幅の数値をより長いビット幅のレジスタに格納する際に、上位ビットをどのように埋めるかという概念です。

  • ゼロ拡張: 符号なし整数(uint8, uint16など)を拡張する際に使用されます。元の数値の最上位ビットに関わらず、新しい上位ビットをすべて0で埋めます。これにより、数値の非負性が保たれます。 例: 8ビットの0b10101010 (170) を32ビットにゼロ拡張すると 0b00000000000000000000000010101010 となります。

  • 符号拡張: 符号付き整数(int8, int16など)を拡張する際に使用されます。元の数値の最上位ビット(符号ビット)の値を、新しい上位ビットすべてにコピーして埋めます。これにより、数値の符号と値が保たれます。 例: 8ビットの0b10101010 (-86) を32ビットに符号拡張すると 0b11111111111111111111111110101010 となります。

これらの拡張操作は、異なるビット幅の数値間で演算を行う際に非常に重要です。

4. ピーホール最適化 (Peephole Optimization)

ピーホール最適化は、コンパイラの最適化手法の一つで、生成されたアセンブリコードの小さな「窓(peephole)」を覗き込み、非効率な命令シーケンスをより効率的なものに置き換えるものです。通常、局所的な最適化であり、隣接する数命令のパターンを認識して変換します。

このコミットでは、5gコンパイラのピーホールパスが改善され、冗長なゼロ/符号拡張命令を排除する能力が向上しています。例えば、「値をレジスタにロードして拡張し、すぐにそのレジスタの値を別のレジスタに移動して再度拡張する」といった冗長なパターンを検出し、後者の拡張を不要なものとして削除することができます。

技術的詳細

このコミットの核心は、Goコンパイラとリンカが短い整数型(8ビット、16ビット)をレジスタで扱う方法のセマンティクスを明確化し、それに基づいて最適化を改善した点にあります。

1. MOVB, MOVH のセマンティクス変更

以前は、MOVBMOVHといった命令が、ソースとデスティネーションの型に応じて暗黙的にゼロ拡張または符号拡張を行うことがありました。このコミットにより、これらの命令は「プレーンな移動(plain moves)」として扱われるようになります。つまり、レジスタ内の既に拡張された値の切り捨て(truncation)や、レジスタ間の単純な値の移動を意味するようになります。

これにより、コンパイラはMOVB/MOVH命令を生成する際に、拡張のセマンティクスについて考慮する必要がなくなり、よりシンプルで予測可能なコード生成が可能になります。

2. 新しい擬似命令の導入

拡張のセマンティクスを明示的にするために、以下の擬似命令が導入されました。

  • MOVBS (Move Byte Sign-extended): バイト値をレジスタにロードする際に、符号拡張を強制します。
  • MOVBU (Move Byte Zero-extended): バイト値をレジスタにロードする際に、ゼロ拡張を強制します。
  • MOVHS (Move Halfword Sign-extended): ハーフワード値をレジスタにロードする際に、符号拡張を強制します。
  • MOVHU (Move Halfword Zero-extended): ハーフワード値をレジスタにロードする際に、ゼロ拡張を強制します。

これらの命令は、レジスタに格納される8ビットまたは16ビットの値が、常にその型に応じてゼロ拡張または符号拡張されるという前提を明確にします。これにより、コンパイラはレジスタ内の値の正確な状態を把握しやすくなり、後続の演算や移動で不必要な拡張を避けることができます。

3. リンカの適応

5lリンカは、新しいセマンティクスに合わせてMOVBMOVHを通常のアセンブリmov命令に変換するように適応されました。これにより、コンパイラが生成した擬似命令が、最終的な機械語に正しくマッピングされます。

4. ピーホール最適化の改善

5gコンパイラのピーホールパス(peep.c)が強化され、冗長なゼロ/符号拡張を排除する能力が向上しました。特に、MOVBS x, R; MOVBS R, R' のような連続する拡張命令のパターンを検出し、後続の拡張をより効率的な単純な移動(MOVB R, R')に置き換えることができるようになりました。これは、レジスタRに既に適切に拡張された値が格納されている場合、再度同じ種類の拡張を行う必要がないためです。この最適化により、生成されるコードの命令数が削減され、実行効率が向上します。

5. パフォーマンスベンチマークの結果

コミットメッセージには、encoding/binary, crypto/des, strconvパッケージにおけるベンチマーク結果が示されています。多くのベンチマークでパフォーマンスが向上しており、特にcrypto/desBenchmarkEncryptBenchmarkDecryptでは25%以上の大幅な改善が見られます。これは、短い整数型に対する操作がこれらのパッケージで頻繁に行われており、今回の最適化が直接的に効果を発揮したことを示しています。

  • encoding/binary: BenchmarkPutUvarint32BenchmarkPutUvarint64で約6-7%の改善。
  • crypto/des: BenchmarkEncryptBenchmarkDecryptで約26%の改善。
  • strconv: BenchmarkAtof系の関数で10-20%程度の改善、BenchmarkAtoi系でも数%の改善。

これらの結果は、今回の変更がGoプログラム全体のパフォーマンス、特に数値演算やデータ変換が頻繁に行われる領域において、顕著な改善をもたらすことを裏付けています。

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

このコミットでは、主に以下のファイルが変更されています。これらはGoコンパイラ(5c, 5g)とリンカ(5l)のバックエンド部分に属します。

  • src/cmd/5c/reg.c: 4行の変更 (+2, -2)
  • src/cmd/5g/cgen.c: 10行の変更 (+10, -0)
  • src/cmd/5g/ggen.c: 9行の変更 (+9, -0)
  • src/cmd/5g/gsubr.c: 49行の変更 (+33, -16)
  • src/cmd/5g/peep.c: 103行の変更 (+70, -33)
  • src/cmd/5g/reg.c: 4行の変更 (+2, -2)
  • src/cmd/5l/asm.c: 2行の変更 (+2, -0)
  • src/cmd/5l/optab.c: 4行の変更 (+2, -2)

合計で8ファイルが変更され、136行が追加され、49行が削除されています。

コアとなるコードの解説

src/cmd/5c/reg.c および src/cmd/5g/reg.c

これらのファイルは、レジスタ割り当てと命令生成に関連する部分です。変更点では、TCHAR/TUCHAR(8ビット)やTSHORT/TUSHORT(16ビット)といった型に対して、従来のAMOVB/AMOVHではなく、明示的に符号拡張を行うAMOVBS/AMOVHSを使用するように変更されています。これは、レジスタにロードされる短い整数値が、その型に応じて適切に拡張されるべきであるという新しいセマンティクスを反映しています。

例:

--- a/src/cmd/5c/reg.c
+++ b/src/cmd/5c/reg.c
@@ -559,9 +559,9 @@ addmove(Reg *r, int bn, int rn, int f)
 
  	p1->as = AMOVW;
  	if(v->etype == TCHAR || v->etype == TUCHAR)
- 		p1->as = AMOVB;
+ 		p1->as = AMOVBS;
  	if(v->etype == TSHORT || v->etype == TUSHORT)
- 		p1->as = AMOVH;
+ 		p1->as = AMOVHS;
  	if(v->etype == TFLOAT)
  		p1->as = AMOVF;
  	if(v->etype == TDOUBLE)

src/cmd/5g/cgen.c および src/cmd/5g/ggen.c

これらのファイルは、Go言語のAST(抽象構文木)からアセンブリコードを生成する部分です。短い整数型に対する算術演算(OADD, OSUB, OMUL)やシフト演算(OLSH)の後、結果がレジスタのフルワードに格納されるため、その結果が元の短い型に適切に正規化(ゼロ拡張または符号拡張)されるようにgins(optoas(OAS, n->type), &n1, &n1);のような命令が追加されています。これにより、後続の操作で値のセマンティクスが正しく保たれます。

例:

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -466,6 +466,16 @@ abop:	// asymmetric binary
  		cgen(nl, &n1);
  	}
  	gins(a, &n2, &n1);
+ 	// Normalize result for types smaller than word.
+ 	if(n->type->width < widthptr) {
+ 		switch(n->op) {
+ 		case OADD:
+ 		case OSUB:
+ 		case OMUL:
+ 			gins(optoas(OAS, n->type), &n1, &n1);
+ 			break;
+ 		}
+ 	}
  	gmove(&n1, res);
  	regfree(&n1);
  	if(n2.op != OLITERAL)

src/cmd/5g/gsubr.c

このファイルは、Goコンパイラの汎用サブルーチンを含み、特にgmove関数は異なる型間での値の移動を処理します。このコミットでは、gmove関数内の型変換ロジックが大幅に修正され、MOVB/MOVHが単純な移動として扱われ、MOVBS/MOVBU/MOVHS/MOVHUが明示的な拡張を行うように変更されています。特に、メモリからレジスタへのロードの場合と、レジスタ間の移動の場合で異なる命令が選択されるようになっています。

例:

--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -706,16 +706,24 @@ gmove(Node *f, Node *t)
  	 * integer copy and truncate
  	 */
  	case CASE(TINT8, TINT8):	// same size
+ 		if(!ismem(f)) {
+ 			a = AMOVB;
+ 			break;
+ 		}
  	case CASE(TUINT8, TINT8):
  	case CASE(TINT16, TINT8):	// truncate
  	case CASE(TUINT16, TINT8):
  	case CASE(TINT32, TINT8):
  	case CASE(TUINT32, TINT8):
- 		a = AMOVB;
+ 		a = AMOVBS;
  		break;
 
- 	case CASE(TINT8, TUINT8):
  	case CASE(TUINT8, TUINT8):
+ 		if(!ismem(f)) {
+ 			a = AMOVB;
+ 			break;
+ 		}
+ 	case CASE(TINT8, TUINT8):
  	case CASE(TINT16, TUINT8):
  	case CASE(TUINT16, TUINT8):
  	case CASE(TINT32, TUINT8):

src/cmd/5g/peep.c

このファイルは、Goコンパイラのピーホール最適化パスを実装しています。最も大きな変更が行われたファイルであり、shortpropという新しい関数が追加されています。この関数は、連続する冗長なゼロ/符号拡張命令(例: MOVBS x, R; MOVBS R, R')を検出し、後続の拡張を単純な移動命令(MOVB R, R')に置き換えることで最適化を行います。これにより、不必要な命令が削減され、コードの効率が向上します。

また、以前のピーホールパスでMOVB/MOVH系の命令に対して行われていた冗長な最適化ロジックが削除されています。

例: shortprop関数の追加

--- a/src/cmd/5g/peep.c
+++ b/src/cmd/5g/peep.c
@@ -587,6 +578,64 @@ constprop(Adr *c1, Adr *v1, Reg *r)
 	}
 }
 
+/*
+ * shortprop eliminates redundant zero/sign extensions.
+ *
+ *   MOVBS x, R
+ *   <no use R>
+ *   MOVBS R, R'
+ *
+ * changed to
+ *
+ *   MOVBS x, R
+ *   ...
+ *   MOVB  R, R' (compiled to mov)
+ *
+ * MOVBS above can be a MOVBS, MOVBU, MOVHS or MOVHU.
+ */
+int
+shortprop(Reg *r)
+{
+	Prog *p, *p1;
+	Reg *r1;
+
+	p = r->prog;
+	r1 = findpre(r, &p->from);
+	if(r1 == R)
+		return 0;
+
+	p1 = r1->prog;
+	if(p1->as == p->as) {
+		// Two consecutive extensions.
+		goto gotit;
+	}
+
+	if(p1->as == AMOVW && isdconst(&p1->from)
+	   && p1->from.offset >= 0 && p1->from.offset < 128) {
+		// Loaded an immediate.
+		goto gotit;
+	}
+
+	return 0;
+
+gotit:
+	if(debug['P'])
+		print("shortprop\n%P\n%P", p1, p);
+	switch(p->as) {
+	case AMOVBS:
+	case AMOVBU:
+		p->as = AMOVB;
+		break;
+	case AMOVHS:
+	case AMOVHU:
+		p->as = AMOVH;
+		break;
+	}
+	if(debug['P'])
+		print(" => %A\n", p->as);
+	return 1;
+}
+
 /*
  * ASLL x,y,w
  * .. (not use w, not set x y w)

src/cmd/5l/asm.c および src/cmd/5l/optab.c

これらのファイルは、Goリンカの命令アセンブルと命令テーブルに関連します。asm.cでは、AMOVBAMOVHAMOVW(通常のワード移動)と同じ命令グループとして扱われるように変更されています。これは、MOVB/MOVHがプレーンな移動命令として扱われるという新しいセマンティクスをリンカに反映させるためです。

optab.cでは、AMOVBAMOVHの命令のエンコーディング情報が変更され、より効率的な命令として扱われるようになっています。特に、AMOVBAMOVHsize14から1に、param8から4に変更されており、これはこれらの命令がより単純な形式でアセンブルされることを示唆しています。

例: src/cmd/5l/optab.c

--- a/src/cmd/5l/optab.c
+++ b/src/cmd/5l/optab.c
@@ -94,10 +94,10 @@ Optab	optab[] =
 	{ AMVN,		C_LCON,	C_NONE,	C_REG,		13, 8, 0,	LFROM },
 	{ ACMP,		C_LCON,	C_REG,	C_NONE,		13, 8, 0,	LFROM },
 
-	{ AMOVB,	C_REG,	C_NONE,	C_REG,		14, 8, 0 },
+	{ AMOVB,	C_REG,	C_NONE,	C_REG,		 1, 4, 0 },
 	{ AMOVBS,	C_REG,	C_NONE,	C_REG,		14, 8, 0 },
 	{ AMOVBU,	C_REG,	C_NONE,	C_REG,		58, 4, 0 },
-	{ AMOVH,	C_REG,	C_NONE,	C_REG,		14, 8, 0 },
+	{ AMOVH,	C_REG,	C_NONE,	C_REG,		 1, 4, 0 },
 	{ AMOVHS,	C_REG,	C_NONE,	C_REG,		14, 8, 0 },
 	{ AMOVHU,	C_REG,	C_NONE,	C_REG,		14, 8, 0 },
 

関連リンク

参考にした情報源リンク