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

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

このコミットは、Goコンパイラのcmd/5g(ARMアーキテクチャ向けコンパイラ)における最適化に関するものです。具体的には、定数を用いた算術演算(asop)において、一時レジスタの使用を避けることで、生成されるアセンブリコードの効率を向上させています。これにより、コンパイル後のバイナリサイズがわずかに削減され、実行時のパフォーマンスが向上する可能性があります。

コミット

commit bbccfddb84473c10e95b1b80f8c6f68a8238a7d5
Author: Dave Cheney <dave@cheney.net>
Date:   Sat Oct 6 21:01:49 2012 +1000

    cmd/5g: avoid temporary during constant asop
    
    func add() int {
            var a int
            a += 10
            a += 20
            a += 30
            a -= 10
            a -= 20
            a -= 30
            return a
    }
    
    before
    
    --- prog list "add" ---
    0000 (/home/dfc/src/add.go:5) TEXT      add+0(SB),$0-4
    0001 (/home/dfc/src/add.go:6) MOVW      $0,R2
    0002 (/home/dfc/src/add.go:7) MOVW      $10,R0
    0003 (/home/dfc/src/add.go:7) ADD       R0,R2,R1
    0004 (/home/dfc/src/add.go:8) MOVW      $20,R0
    0005 (/home/dfc/src/add.go:8) ADD       R0,R1
    0006 (/home/dfc/src/add.go:9) MOVW      $30,R0
    0007 (/home/dfc/src/add.go:9) ADD       R0,R1
    0008 (/home/dfc/src/add.go:10) MOVW     $10,R0
    0009 (/home/dfc/src/add.go:10) SUB      R0,R1
    0010 (/home/dfc/src/add.go:11) MOVW     $20,R0
    0011 (/home/dfc/src/add.go:11) SUB      R0,R1
    0012 (/home/dfc/src/add.go:12) MOVW     $30,R0
    0013 (/home/dfc/src/add.go:12) SUB      R0,R1,R2
    0014 (/home/dfc/src/add.go:12) MOVW     R2,R0
    0015 (/home/dfc/src/add.go:13) MOVW     R2,R1
    0016 (/home/dfc/src/add.go:13) MOVW     R2,.noname+0(FP)
    0017 (/home/dfc/src/add.go:13) RET      ,
    
    after
    
    --- prog list "add" ---
    0000 (/home/dfc/src/add.go:5) TEXT      add+0(SB),$0-4
    0001 (/home/dfc/src/add.go:6) MOVW      $0,R0
    0002 (/home/dfc/src/add.go:7) ADD       $10,R0
    0003 (/home/dfc/src/add.go:8) ADD       $20,R0
    0004 (/home/dfc/src/add.go:9) ADD       $30,R0
    0005 (/home/dfc/src/add.go:10) SUB      $10,R0
    0006 (/home/dfc/src/add.go:11) SUB      $20,R0
    0007 (/home/dfc/src/add.go:12) SUB      $30,R0,R2
    0008 (/home/dfc/src/add.go:13) MOVW     R2,R0
    0009 (/home/dfc/src/add.go:13) MOVW     R2,.noname+0(FP)
    0010 (/home/dfc/src/add.go:13) RET      ,
    
    R=rsc, minux.ma, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/6584056
---\n src/cmd/5g/ggen.c | 11 ++++++++---\n 1 file changed, 8 insertions(+), 3 deletions(-)\n\ndiff --git a/src/cmd/5g/ggen.c b/src/cmd/5g/ggen.c
index 85f63a2f7a..09e8550506 100644
--- a/src/cmd/5g/ggen.c
+++ b/cmd/5g/ggen.c
@@ -371,14 +371,19 @@ cgen_asop(Node *n)
  	case OOR:
  	\ta = optoas(n->etype, nl->type);\n \t\tif(nl->addable) {
-\t\t\tregalloc(&n3, nr->type, N);\n-\t\t\tcgen(nr, &n3);\n+\t\t\tif(smallintconst(nr))\n+\t\t\t\tn3 = *nr;\n+\t\t\telse {\n+\t\t\t\tregalloc(&n3, nr->type, N);\n+\t\t\t\tcgen(nr, &n3);\n+\t\t\t}\n \t\t\tregalloc(&n2, nl->type, N);\n \t\t\tcgen(nl, &n2);\n \t\t\tgins(a, &n3, &n2);\n \t\t\tcgen(&n2, nl);\n \t\t\tregfree(&n2);\n-\t\t\tregfree(&n3);\n+\t\t\tif(n3.op != OLITERAL)\n+\t\t\t\tregfree(&n3);\n \t\t\tgoto ret;\n \t\t}\n \t\tif(nr->ullman < UINF)\n```

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

[https://github.com/golang/go/commit/bbccfddb84473c10e95b1b80f8c6f68a8238a7d5](https://github.com/golang/go/commit/bbccfddb84473c10e95b1b80f8c6f68a8238a7d5)

## 元コミット内容

このコミットは、Goコンパイラの`cmd/5g`(ARMアーキテクチャ向け)において、定数を含む算術代入演算(`+=`, `-=` など)のコード生成を最適化するものです。具体的には、定数オペランドに対して一時レジスタを割り当てることを避け、直接レジスタに定数をロードして演算を行うように変更しています。

コミットメッセージには、以下のGoコード例と、そのコンパイル前後のアセンブリコードが示されています。

```go
func add() int {
        var a int
        a += 10
        a += 20
        a += 30
        a -= 10
        a -= 20
        a -= 30
        return a
}

変更前のアセンブリ(抜粋): 変更前は、各定数(10, 20, 30)をMOVW命令で一時レジスタR0にロードし、そのR0と変数aが格納されているレジスタ(R2R1)を使ってADDSUB演算を行っていました。例えば、a += 10は以下のようになります。

0002 MOVW $10,R0  // 定数10をR0にロード
0003 ADD  R0,R2,R1 // R2 (a) と R0 (10) を加算し、結果をR1に格納

変更後のアセンブリ(抜粋): 変更後は、定数を直接ADDSUB命令のオペランドとして使用することで、一時レジスタR0へのロードが不要になっています。

0002 ADD $10,R0 // R0 (a) に直接定数10を加算

これにより、MOVW命令の数が減り、より効率的なコードが生成されています。

変更の背景

この変更の背景には、コンパイラが生成するコードの効率化と最適化があります。特に、組み込みシステムやモバイルデバイスなど、リソースが限られた環境で動作するプログラムにおいては、生成されるバイナリのサイズや実行速度が重要になります。

Goコンパイラは、ソースコードを機械語に変換する際に様々な最適化を行います。その一つがレジスタ割り当ての最適化です。CPUのレジスタは非常に高速な記憶領域であり、メモリへのアクセスよりもはるかに高速です。そのため、頻繁にアクセスされる変数や中間結果をできるだけレジスタに保持することで、プログラムの実行速度を向上させることができます。

このコミットが行われる前は、Goコンパイラ(特に5g)が定数を含む算術代入演算を処理する際に、不必要に一時レジスタを使用するケースがありました。具体的には、定数をレジスタにロードしてから演算を行うという手順を踏んでいました。これは、定数を直接演算命令の即値オペランドとして使用できる場合でも行われていました。

この「一時レジスタの回避」は、以下のメリットをもたらします。

  1. 命令数の削減: 一時レジスタへのロード命令が不要になるため、生成されるアセンブリ命令の総数が減少します。
  2. バイナリサイズの削減: 命令数が減ることで、最終的な実行可能ファイルのサイズがわずかに小さくなります。
  3. 実行速度の向上: 不要なレジスタ操作がなくなることで、CPUのパイプライン効率が向上し、実行速度がわずかに速くなる可能性があります。また、レジスタの競合が減り、より重要な変数にレジスタを割り当てやすくなることも期待できます。

このような小さな最適化の積み重ねが、コンパイラ全体のコード生成品質を向上させ、Go言語で書かれたプログラムのパフォーマンスに貢献します。

前提知識の解説

Goコンパイラ 5g

Go言語の初期のコンパイラ群は、ターゲットアーキテクチャごとに異なる名前が付けられていました。

  • 8g: Intel 386 (32-bit x86) アーキテクチャ向け
  • 6g: AMD64 (64-bit x86) アーキテクチャ向け
  • 5g: ARM (32-bit ARM) アーキテクチャ向け

これらのコンパイラは、Goの公式コンパイラツールチェーンであるgc(Go compiler)スイートの一部でした。現在では、これらのコンパイラは統合され、go buildコマンドが自動的に適切なバックエンドを選択するようになっていますが、このコミットが作成された2012年当時は、5gがARMアーキテクチャ向けの主要なコンパイラでした。

アセンブリ言語 (Goの抽象アセンブリ)

Goのアセンブリ言語は、一般的なCPUのアセンブリ言語とは少し異なります。Goのアセンブラは、Plan 9オペレーティングシステムのアセンブラに由来しており、特定のCPUの命令セットに直接対応するのではなく、より抽象的な命令セットを使用します。これは、Goコンパイラが異なるアーキテクチャ間でコード生成のロジックを共有しやすくするためです。

  • MOVW: "Move Word" の略で、32ビットのデータを移動する命令です。Goのアセンブリでは、レジスタ間、レジスタとメモリ間、または即値(定数)をレジスタにロードする際に使用されます。
  • ADD: 加算命令です。オペランドとしてレジスタや即値を取り、加算結果をレジスタに格納します。
  • SUB: 減算命令です。オペランドとしてレジスタや即値を取り、減算結果をレジスタに格納します。
  • R0, R1, R2 など: ARMアーキテクチャにおける汎用レジスタです。Goのアセンブリでは、これらのレジスタが変数や中間結果の保持に使用されます。
  • $0, $10 など: 即値(リテラル定数)を表します。アセンブリ命令に直接埋め込まれる値です。

定数伝播 (Constant Propagation)

定数伝播は、コンパイラの最適化手法の一つです。プログラム中で値がコンパイル時に確定している変数や式(定数)がある場合、その定数値をプログラムの他の場所でその変数や式が使われている箇所に直接置き換えることで、実行時の計算を減らすことができます。

例えば、x = 10; y = x + 5; というコードがあった場合、定数伝播によって y = 10 + 5; となり、さらに定数畳み込み(Constant Folding)によって y = 15; と最適化されます。このコミットは、定数伝播と密接に関連しており、定数であることが分かっている値をより効率的にアセンブリコードに反映させることを目的としています。

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

レジスタ割り当ては、コンパイラの最も重要な最適化の一つです。CPUには限られた数のレジスタしかありませんが、レジスタへのアクセスはメモリへのアクセスよりもはるかに高速です。レジスタ割り当ての目的は、プログラムの実行中に頻繁に使用される変数や中間結果を、効率的にCPUのレジスタに割り当てることです。

もしレジスタが不足した場合、コンパイラは一部の値をメモリ(スタック)に「スピル」(spill)する必要があります。スピルはパフォーマンスの低下を招くため、コンパイラはできるだけスピルを避けるようにレジスタを割り当てます。

このコミットは、定数オペランドに対して不必要な一時レジスタの割り当てを避けることで、レジスタの利用効率を高め、結果として全体的なレジスタ割り当ての品質を向上させることに貢献します。

asop (Arithmetic/Shift Operation)

asopは、"Arithmetic/Shift Operation" の略で、算術演算(加算、減算、乗算、除算など)やビットシフト演算(左シフト、右シフトなど)を総称する用語です。コンパイラのコード生成フェーズでは、これらの操作に対応するアセンブリ命令が生成されます。このコミットでは、特に定数を含むasopの最適化に焦点を当てています。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのcmd/5gが、定数を含む算術代入演算(+=, -= など)を処理する際のコード生成ロジックを改善した点にあります。

以前の5gコンパイラでは、a += 10のようなコードがあった場合、まず定数10を一時レジスタにロードし、その一時レジスタと変数aが格納されているレジスタを使って加算命令を生成していました。これは、以下のような手順に相当します。

  1. 定数10を一時レジスタ(例: R0)に移動 (MOVW $10, R0)。
  2. 変数aのレジスタ(例: R2)と一時レジスタR0の内容を加算し、結果を変数aのレジスタに格納 (ADD R0, R2, R1 または ADD R0, R1)。

このアプローチは、定数が非常に大きい場合や、複雑な式の一部である場合には適切かもしれませんが、ARMアーキテクチャのように即値オペランドを直接命令に埋め込むことができる場合(特に小さな定数)、一時レジスタの使用は冗長であり、非効率的です。

このコミットでは、src/cmd/5g/ggen.cファイルのcgen_asop関数内のロジックが変更されました。cgen_asopは、算術演算やシフト演算のコードを生成する役割を担っています。変更のポイントは、右オペランド(nr)が「小さな整数定数」であるかどうかをsmallintconst(nr)関数でチェックするようになったことです。

  • smallintconst(nr)が真の場合: 右オペランドが小さな整数定数であれば、一時レジスタを割り当てる代わりに、その定数値を直接命令のオペランドとして使用します。これにより、MOVW命令とそれに伴う一時レジスタの割り当て・解放が不要になります。コード生成の際には、定数ノード*nrを直接n3として使用します。
  • smallintconst(nr)が偽の場合: 右オペランドが小さな整数定数でない場合(例えば、大きな定数、変数、または複雑な式の結果である場合)は、以前と同様に一時レジスタを割り当て、そのレジスタに値をロードしてから演算を行います。

この最適化により、コンパイラはより簡潔で効率的なアセンブリコードを生成できるようになります。特に、ループ内で頻繁に定数との加減算が行われるようなケースでは、この最適化がパフォーマンスに寄与する可能性があります。

また、この変更はレジスタ割り当ての観点からも重要です。不必要な一時レジスタの使用を避けることで、限られたCPUレジスタをより有効に活用できるようになります。これにより、レジスタスピル(レジスタの値をメモリに退避させること)の発生を減らし、全体的なプログラムの実行効率を向上させることが期待されます。

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

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

diff --git a/src/cmd/5g/ggen.c b/src/cmd/5g/ggen.c
index 85f63a2f7a..09e8550506 100644
--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -371,14 +371,19 @@ cgen_asop(Node *n)
  	case OOR:
  	\ta = optoas(n->etype, nl->type);\n \t\tif(nl->addable) {
-\t\t\tregalloc(&n3, nr->type, N);\n-\t\t\tcgen(nr, &n3);\n+\t\t\tif(smallintconst(nr))\n+\t\t\t\tn3 = *nr;\n+\t\t\telse {\n+\t\t\t\tregalloc(&n3, nr->type, N);\n+\t\t\t\tcgen(nr, &n3);\n+\t\t\t}\n \t\t\tregalloc(&n2, nl->type, N);\n \t\t\tcgen(nl, &n2);\n \t\t\tgins(a, &n3, &n2);\n \t\t\tcgen(&n2, nl);\n \t\t\tregfree(&n2);\n-\t\t\tregfree(&n3);\n+\t\t\tif(n3.op != OLITERAL)\n+\t\t\t\tregfree(&n3);\n \t\t\tgoto ret;\n \t\t}\
 \t\tif(nr->ullman < UINF)\

コアとなるコードの解説

この変更は、cgen_asop関数内で、算術代入演算(+=, -= など)の右オペランド(nr)が定数である場合のコード生成ロジックを改善しています。

  1. if(nl->addable) ブロック: このブロックは、左オペランド(nl)がレジスタに直接加算可能な場合に実行されるコードパスです。つまり、a += Xa の部分がレジスタに直接対応できるようなケースです。

  2. 変更前の動作: 変更前は、右オペランドnrが何であっても、まずregalloc(&n3, nr->type, N);で一時レジスタn3を割り当て、cgen(nr, &n3);nrの値をその一時レジスタn3に生成していました。その後、gins(a, &n3, &n2);で実際の演算を行い、最後にregfree(&n3);で一時レジスタを解放していました。

  3. 変更後の動作:

    • if(smallintconst(nr)): 新たに導入された条件分岐です。smallintconst(nr)は、右オペランドnrが「小さな整数定数」であるかどうかを判定する関数です。Goコンパイラでは、特定の範囲内の整数定数は、アセンブリ命令の即値オペランドとして直接使用できる場合があります。
    • n3 = *nr;: nrが小さな整数定数である場合、一時レジスタを割り当てる代わりに、n3nrノード自体をコピーします。これにより、n3はレジスタではなく、定数ノードとして扱われます。
    • else { regalloc(&n3, nr->type, N); cgen(nr, &n3); }: nrが小さな整数定数でない場合は、以前と同様に一時レジスタn3を割り当て、nrの値をそこに生成します。
    • if(n3.op != OLITERAL) regfree(&n3);: レジスタ解放のロジックも変更されました。n3.op != OLITERALという条件が追加されています。
      • OLITERALは、ノードがリテラル(定数)を表すことを示すオペレーションコードです。
      • もしn3が定数ノード(OLITERAL)であれば、それはレジスタを割り当てられたものではないため、regfree(レジスタ解放)は不要です。
      • n3がレジスタに割り当てられたものであれば(OLITERALでない場合)、regfree(&n3);でレジスタを解放します。

この変更の意義: この変更により、コンパイラは定数を含む算術代入演算において、不必要な一時レジスタの割り当てと解放を避けることができます。これにより、生成されるアセンブリコードの命令数が削減され、より効率的なバイナリが生成されます。これは、コンパイラの最適化における「レジスタの有効活用」と「命令数の削減」という重要な原則に基づいています。

関連リンク

参考にした情報源リンク