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

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

このコミットは、Goコンパイラツールチェーン(6c, 6g, 6l)において、64ビット値から32ビット値への明示的な切り捨て(truncation)を導入するためにMOVQL命令を追加するものです。これにより、コンパイラの最適化フェーズであるコピー伝播(copy propagation)が、意図しない32ビット切り捨てを64ビット値の使用箇所に伝播させ、誤った結果を引き起こす問題を解決します。

コミット

commit e530d6a1e00fbc0149b71bca9f940058838c1c44
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 10 12:51:59 2012 -0400

    6c, 6g, 6l: add MOVQL to make truncation explicit
    
    Without an explicit signal for a truncation, copy propagation
    will sometimes propagate a 32-bit truncation and end up
    overwriting uses of the original 64-bit value.
    
    The case that arose in practice is in C but I believe
    that the same could plausibly happen in Go.
    The main reason we didn't run into the same in Go
    is that I (perhaps incorrectly?) drop MOVL AX, AX
    during gins, so the truncation was never generated, so
    it didn't confuse the optimizer.
    
    Fixes #1315.
    Fixes #3488.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6002043

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

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

元コミット内容

このコミットは、Goコンパイラのバックエンドにおいて、64ビットから32ビットへの値の切り捨てを明示的に示すための新しいアセンブリ命令MOVQLを導入します。これにより、コンパイラの最適化パス、特にコピー伝播が、本来64ビットとして扱われるべき値に対して誤って32ビットの切り捨てを適用してしまう問題を修正します。

具体的には、以下のような状況で問題が発生していました。

  1. 64ビットの値を32ビットの変数に代入する際に、値が切り捨てられる。
  2. コンパイラのコピー伝播最適化が、この32ビットへの切り捨て操作を、元の64ビット値が使用されている他の箇所にまで伝播させてしまう。
  3. 結果として、本来64ビットとして扱われるべき場所で値が誤って切り捨てられ、不正な動作やバグを引き起こす。

この問題はC言語のコードで実際に発生しましたが、Go言語でも同様の状況が発生する可能性が指摘されています。Go言語でこれまでこの問題が顕在化しなかったのは、MOVL AX, AXのような明示的な切り捨て命令がgins(Goの命令生成フェーズ)で削除されていたため、最適化器が混乱する機会がなかったからだと説明されています。

この変更は、Issue #1315とIssue #3488を修正します。

変更の背景

この変更の背景には、コンパイラの最適化、特に「コピー伝播」が引き起こす潜在的なバグがあります。コピー伝播は、ある変数の値が別の変数にコピーされた後、元の変数の代わりにコピー先の変数を使用するようにコードを書き換える最適化手法です。これは通常、パフォーマンス向上に寄与しますが、型変換、特にサイズの異なる型への変換(例: 64ビットから32ビットへの切り捨て)が絡むと、意図しない副作用を生じることがあります。

具体的には、64ビットの整数値を32ビットの整数変数に代入する際、上位32ビットが切り捨てられます。この切り捨て操作が明示的にコンパイラに伝わらない場合、コンパイラは元の64ビット値と切り捨てられた32ビット値を区別できなくなり、最適化の過程で64ビット値が使われるべき場所で誤って32ビットに切り捨てられた値を使ってしまう可能性がありました。

この問題は、Goコンパイラが生成するアセンブリコードにおいて、64ビットから32ビットへの切り捨てが十分に明示されていなかったことに起因します。既存の命令では、この切り捨てが単なるデータ移動と区別されにくく、最適化器が誤った推論を行う余地がありました。

前提知識の解説

コンパイラの最適化

コンパイラの最適化とは、ソースコードを機械語に変換する際に、生成される機械語コードの実行速度やサイズを改善するプロセスです。様々な最適化手法がありますが、このコミットに関連するのは以下の概念です。

  • コピー伝播 (Copy Propagation): ある変数xが別の変数yにコピーされた場合(例: y = x;)、その後のコードでxが使われている箇所をyに置き換える最適化です。これにより、余分なロード/ストア命令を削減し、レジスタの有効活用を促進します。
  • データフロー解析 (Data Flow Analysis): プログラムの実行中にデータがどのように流れるかを分析する技術です。最適化器はこれを用いて、変数の値がどこで定義され、どこで使われているか、その値がプログラムのどの時点で有効であるかなどを判断します。
  • 型システムと型変換 (Type System and Type Conversion): プログラミング言語における型の概念と、ある型から別の型へ値を変換する操作です。Go言語では、異なるサイズの整数型間での代入は、必要に応じて切り捨てや符号拡張が行われます。

アセンブリ言語と命令

アセンブリ言語は、CPUが直接実行できる機械語命令を人間が読める形式で記述したものです。各CPUアーキテクチャには固有の命令セットがあります。このコミットはx86-64アーキテクチャ(64ビットIntel/AMDプロセッサ)を対象としています。

  • MOV命令: データを移動させる基本的な命令です。例えば、MOV AX, BXはBXレジスタの値をAXレジスタにコピーします。
  • レジスタ (Registers): CPU内部にある高速な記憶領域で、演算の対象となるデータを一時的に保持します。x86-64アーキテクチャでは、AX, BX, CX, DXなどの汎用レジスタや、RAX, RBXなどの64ビットレジスタがあります。
  • 切り捨て (Truncation): 広いビット幅の値を狭いビット幅の値に変換する際に、上位ビットを破棄する操作です。例えば、64ビットの値を32ビットに切り捨てると、上位32ビットの情報が失われます。

Goコンパイラツールチェーン

Go言語のコンパイラツールチェーンは、主に以下のコンポーネントで構成されます(このコミット当時の名称)。

  • 6c: C言語のソースファイルをコンパイルするためのツール。Go言語のランタイムや標準ライブラリの一部はC言語で書かれており、それらをコンパイルするために使用されます。
  • 6g: Go言語のソースファイルをコンパイルするためのツール。Goのソースコードをアセンブリコードに変換します。
  • 6l: リンカ。コンパイルされたオブジェクトファイルやライブラリを結合して実行可能ファイルを生成します。

これらのツールは、Go言語のクロスコンパイル能力を支えるために、ターゲットアーキテクチャ(この場合はx86-64)に応じてプレフィックス(例: 6はx86-64、8はx86-32、5はARMなど)が付けられています。

技術的詳細

このコミットの核心は、64ビットから32ビットへの明示的な切り捨てを表現する新しいアセンブリ命令MOVQLの導入です。

従来のGoコンパイラでは、64ビット値を32ビット変数に代入する際、例えばAMOVL(Move Long)のような命令が使用されていました。しかし、AMOVLは単に32ビットのデータを移動させる命令であり、それが「64ビット値の切り捨て結果である」というセマンティクスを明示的に持っていませんでした。この曖昧さが、コンパイラの最適化器、特にコピー伝播に誤った推論をさせていました。

最適化器は、あるレジスタに格納された64ビット値が、その後32ビットのレジスタに移動された場合、その32ビット値が元の64ビット値の単なる部分であると解釈し、元の64ビット値が使われるべき場所で、誤って32ビットに切り捨てられた値を伝播させてしまう可能性がありました。これは、特に元の64ビット値がその後も64ビットとして使用される場合に問題となります。

MOVQL命令は、この問題を解決するために導入されました。MOVQLは「Move Quadword to Longword with Truncation」のような意味合いを持ち、64ビットのソースから32ビットのデスティネーションへの移動が、明示的な切り捨て操作であることをコンパイラに伝えます。これにより、最適化器はMOVQL命令を見たときに、その結果が元の64ビット値とは異なる、切り捨てられた32ビット値であることを正確に認識できます。

この変更は、Goコンパイラの以下の部分に影響を与えます。

  1. 命令セットの拡張: src/cmd/6l/6.out.hAMOVQLが新しいアセンブリ命令として定義されます。
  2. リンカの命令テーブル: src/cmd/6l/optab.cAMOVQLのオペコードと処理が追加され、リンカがこの新しい命令を正しく処理できるようになります。
  3. コード生成: src/cmd/6c/txt.csrc/cmd/6g/gsubr.cにおいて、64ビットから32ビットへの型変換が必要な場合に、従来のAMOVLの代わりにAMOVQLが生成されるようになります。これにより、コンパイラが明示的な切り捨て命令を発行するようになります。
  4. 最適化器の認識: src/cmd/6c/peep.c, src/cmd/6c/reg.c, src/cmd/6g/peep.c, src/cmd/6g/reg.cなどの最適化関連のファイルでAMOVQLが認識され、コピー伝播やレジスタ割り当てなどの最適化が、この明示的な切り捨てを考慮して行われるようになります。

この修正により、コンパイラは64ビット値と32ビット切り捨て値のセマンティクスを正確に区別できるようになり、最適化による意図しないバグを防ぐことができます。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/cmd/6l/6.out.h: 新しいアセンブリ命令AMOVQLの定義を追加。

    --- a/src/cmd/6l/6.out.h
    +++ b/src/cmd/6l/6.out.h
    @@ -741,6 +741,8 @@ enum	as
     	APREFETCHT1,
     	APREFETCHT2,
     	APREFETCHNTA,
    +	
    +	AMOVQL,
     
     	ALAST
     };
    
  2. src/cmd/6l/optab.c: リンカの命令テーブルにAMOVQLのエントリを追加。これにより、リンカがAMOVQL命令を認識し、対応する機械語を生成できるようになります。

    --- a/src/cmd/6l/optab.c
    +++ b/src/cmd/6l/optab.c
    @@ -1282,6 +1282,8 @@ Optab optab[] =
     	{ APREFETCHT1,	yprefetch,	Pm,	0x18,(02) },
     	{ APREFETCHT2,	yprefetch,	Pm,	0x18,(03) },
     	{ APREFETCHNTA,	yprefetch,	Pm,	0x18,(00) },
    +	
    +	{ AMOVQL,	yrl_ml,	Px, 0x89 },
     
     	{ AEND },
     	0
    
  3. src/cmd/6g/gsubr.c: Go言語のコード生成部分で、64ビットから32ビットへの切り捨てが必要な場合にAMOVQLを生成するように変更。

    --- a/src/cmd/6g/gsubr.c
    +++ b/src/cmd/6g/gsubr.c
    @@ -706,11 +706,14 @@ gmove(Node *f, Node *t)
     	case CASE(TINT32, TUINT32):
     	case CASE(TUINT32, TINT32):
     	case CASE(TUINT32, TUINT32):
    +		a = AMOVL;
    +		break;
    +
     	case CASE(TINT64, TINT32):	// truncate
     	case CASE(TUINT64, TINT32):
     	case CASE(TINT64, TUINT32):
     	case CASE(TUINT64, TUINT32):
    -		a = AMOVL;
    +		a = AMOVQL;
     		break;
     
     	case CASE(TINT64, TINT64):	// same size
    
  4. src/cmd/6c/txt.c: C言語のコード生成部分で、同様に64ビットから32ビットへの切り捨てにAMOVQLを使用するように変更。多くのCASE文からTIND(ポインタ型)関連の行が削除され、AMOVQLが新しいCASEブロックで導入されています。

    --- a/src/cmd/6c/txt.c
    +++ b/src/cmd/6c/txt.c
    @@ -809,7 +809,6 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TCHAR):
     	case CASE(	TLONG,	TCHAR):
     	case CASE(	TULONG,	TCHAR):
    -	case CASE(	TIND,	TCHAR):
     
     	case CASE(	TCHAR,	TUCHAR):
     	case CASE(	TUCHAR,	TUCHAR):
    @@ -819,7 +818,6 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TUCHAR):
     	case CASE(	TLONG,	TUCHAR):
     	case CASE(	TULONG,	TUCHAR):
    -	case CASE(	TIND,	TUCHAR):
     
     	case CASE(	TSHORT,	TSHORT):
     	case CASE(	TUSHORT,TSHORT):
    @@ -827,7 +825,6 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TSHORT):
     	case CASE(	TLONG,	TSHORT):
     	case CASE(	TULONG,	TSHORT):
    -	case CASE(	TIND,	TSHORT):
     
     	case CASE(	TSHORT,	TUSHORT):
     	case CASE(	TUSHORT,TUSHORT):
    @@ -835,42 +832,26 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TUSHORT):
     	case CASE(	TLONG,	TUSHORT):
     	case CASE(	TULONG,	TUSHORT):
    -	case CASE(	TIND,	TUSHORT):
     
     	case CASE(	TINT,	TINT):
     	case CASE(	TUINT,	TINT):
     	case CASE(	TLONG,	TINT):
     	case CASE(	TULONG,	TINT):
    -	case CASE(	TIND,	TINT):
     
     	case CASE(	TINT,	TUINT):
     	case CASE(	TUINT,	TUINT):
     	case CASE(	TLONG,	TUINT):
     	case CASE(	TULONG,	TUINT):
    -	case CASE(	TIND,	TUINT):\n-\n-\tcase CASE(\tTUINT,\tTIND):\n-\tcase CASE(\tTVLONG,\tTUINT):\n-\tcase CASE(\tTVLONG,\tTULONG):\n-\tcase CASE(\tTUVLONG, TUINT):\n-\tcase CASE(\tTUVLONG, TULONG):\n      *****/
     	a = AMOVL;
     	break;
     
    -	case CASE(	TVLONG,	TCHAR):\n-	case	CASE(\tTVLONG,\tTSHORT):\n-	case CASE(\tTVLONG,\tTINT):\n-	case CASE(\tTVLONG,\tTLONG):\n-	case CASE(\tTUVLONG, TCHAR):\n-	case	CASE(\tTUVLONG, TSHORT):\n-	case CASE(\tTUVLONG, TINT):\n-	case CASE(\tTUVLONG, TLONG):\
    +	case CASE(	TINT,	TIND):\
     	case CASE(	TINT,	TVLONG):\
     	case CASE(	TINT,	TUVLONG):\
    -	case CASE(	TLONG,	TVLONG):\
    -	case CASE(	TINT,	TIND):\
     	case CASE(	TLONG,	TIND):\
    +	case CASE(	TLONG,	TVLONG):\
    +	case CASE(	TLONG,	TUVLONG):\
     	a = AMOVLQSX;
     	if(f->op == OCONST) {
     		f->vconst &= (uvlong)0xffffffffU;
    @@ -886,22 +867,53 @@ gmove(Node *f, Node *t)
     	case CASE(	TULONG,	TVLONG):\
     	case CASE(	TULONG,	TUVLONG):\
     	case CASE(	TULONG,	TIND):\
    -	a = AMOVL;	/* same effect as AMOVLQZX */
    +	a = AMOVLQZX;
     	if(f->op == OCONST) {
     		f->vconst &= (uvlong)0xffffffffU;
     		a = AMOVQ;
     	}
     	break;
    +	
    +	case CASE(	TIND,	TCHAR):\
    +	case CASE(	TIND,	TUCHAR):\
    +	case CASE(	TIND,	TSHORT):\
    +	case CASE(	TIND,	TUSHORT):\
    +	case CASE(	TIND,	TINT):\
    +	case CASE(	TIND,	TUINT):\
    +	case CASE(	TIND,	TLONG):\
    +	case CASE(	TIND,	TULONG):\
    +	case CASE(	TVLONG,	TCHAR):\
    +	case CASE(	TVLONG,	TUCHAR):\
    +	case CASE(	TVLONG,	TSHORT):\
    +	case CASE(	TVLONG,	TUSHORT):\
    +	case CASE(	TVLONG,	TINT):\
    +	case CASE(	TVLONG,	TUINT):\
    +	case CASE(	TVLONG,	TLONG):\
    +	case CASE(	TVLONG,	TULONG):\
    +	case CASE(	TUVLONG,	TCHAR):\
    +	case CASE(	TUVLONG,	TUCHAR):\
    +	case CASE(	TUVLONG,	TSHORT):\
    +	case CASE(	TUVLONG,	TUSHORT):\
    +	case CASE(	TUVLONG,	TINT):\
    +	case CASE(	TUVLONG,	TUINT):\
    +	case CASE(	TUVLONG,	TLONG):\
    +	case CASE(	TUVLONG,	TULONG):\
    +	a = AMOVQL;
    +	if(f->op == OCONST) {
    +		f->vconst &= (int)0xffffffffU;
    +		a = AMOVL;
    +	}
    +	break;	
     
    +	case CASE(	TIND,	TIND):\
     	case CASE(	TIND,	TVLONG):\
    -	case CASE(	TVLONG,	TVLONG):\
    -	case CASE(	TUVLONG,	TVLONG):\
    -	case CASE(	TVLONG,	TUVLONG):\
    -	case CASE(	TUVLONG,	TUVLONG):\
     	case CASE(	TIND,	TUVLONG):\
     	case CASE(	TVLONG,	TIND):\
    +	case CASE(	TVLONG,	TVLONG):\
    +	case CASE(	TVLONG,	TUVLONG):\
     	case CASE(	TUVLONG,	TIND):\
    -	case CASE(	TIND,	TIND):\
    +	case CASE(	TUVLONG,	TVLONG):\
    +	case CASE(	TUVLONG,	TUVLONG):\
     	a = AMOVQ;
     	break;
    
  5. src/cmd/6c/peep.c, src/cmd/6c/reg.c, src/cmd/6g/peep.c, src/cmd/6g/reg.c: これらのファイルは、コンパイラの最適化フェーズ(ピーフホール最適化やレジスタ割り当て)に関連しています。AMOVQLが新しい命令として認識され、最適化のロジックに組み込まれるように変更されています。これにより、最適化器がAMOVQLを正しく扱い、コピー伝播などの最適化が意図しない副作用を引き起こさないようになります。

コアとなるコードの解説

このコミットの主要な目的は、64ビットから32ビットへの切り捨て操作をコンパイラが明示的に認識できるようにすることです。これを実現するために、新しいアセンブリ命令AMOVQLが導入され、Goコンパイラのコード生成および最適化パスに統合されました。

  • src/cmd/6l/6.out.hsrc/cmd/6l/optab.c: これらはリンカに関連するファイルで、新しい命令AMOVQLをGoのアセンブリ言語の命令セットに追加し、リンカがその命令を機械語に変換できるようにします。6.out.hは命令の列挙型を定義し、optab.cは各命令に対応するオペコード(機械語のバイト列)と処理関数を定義するテーブルです。AMOVQLがこのテーブルに追加されることで、リンカはAMOVQLを正しく解釈し、実行可能なバイナリに含めることができるようになります。

  • src/cmd/6g/gsubr.csrc/cmd/6c/txt.c: これらのファイルは、Goコンパイラ(6g)とCコンパイラ(6c)のコード生成部分です。gmove関数は、異なる型の間の値の移動(代入)を処理し、適切なアセンブリ命令を生成します。 変更前は、64ビットから32ビットへの切り捨てを伴う代入(例: TINT64からTINT32への変換)に対してAMOVL(Move Long)が生成されていました。しかし、AMOVLは単なる32ビットのデータ移動であり、切り捨てのセマンティクスを明示的に持ちません。 変更後は、これらのケースでAMOVQLが生成されるようになります。AMOVQLは、64ビット値を32ビットに切り捨てるという操作を明示的に示すため、コンパイラの最適化器がこの操作を正確に理解できるようになります。 src/cmd/6c/txt.cでは、TIND(ポインタ型)から整数型への変換に関する多くのCASE文が削除され、TVLONG(64ビット整数)やTUVLONG(符号なし64ビット整数)からより小さい整数型への変換、およびTINDから整数型への変換に対してAMOVQLが導入されています。これは、ポインタが64ビットであるシステムにおいて、ポインタ値を整数型に変換する際に切り捨てが発生しうるため、その場合も明示的なMOVQLを使用するようにしたものです。

  • src/cmd/6c/peep.c, src/cmd/6c/reg.c, src/cmd/6g/peep.c, src/cmd/6g/reg.c: これらのファイルは、コンパイラの最適化フェーズ(ピーフホール最適化やレジスタ割り当て)に関連しています。ピーフホール最適化は、生成されたアセンブリコードの小さなパターンをより効率的なコードに置き換えるものです。レジスタ割り当ては、プログラムの変数をCPUのレジスタに効率的に割り当てることで、メモリアクセスを減らし、実行速度を向上させます。 これらの最適化器は、プログラムのデータフローを分析し、コピー伝播などの最適化を適用します。AMOVQLがこれらのファイルで認識されるように変更されたことで、最適化器はAMOVQLが明示的な切り捨て操作であることを理解し、その結果として生成される32ビット値が、元の64ビット値とは異なるセマンティクスを持つことを考慮するようになります。これにより、最適化器が誤って64ビット値の代わりに切り捨てられた32ビット値を伝播させることを防ぎ、バグの発生を抑制します。

この一連の変更により、Goコンパイラは64ビットから32ビットへの型変換をより正確に扱い、最適化による潜在的なバグを回避できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のIssueトラッカー (GitHub): 上記のIssueリンクから詳細な議論や背景情報を参照しました。
  • Go言語のソースコード: コミットに含まれるファイルパスから、Goコンパイラの内部構造と各ファイルの役割を理解しました。
  • コンパイラ最適化に関する一般的な知識: コピー伝播、データフロー解析、ピーフホール最適化などの概念は、コンパイラ理論の一般的な知識に基づいています。
  • x86-64アセンブリ言語の知識: MOV命令やレジスタの概念は、x86-64アセンブリ言語の一般的な知識に基づいています。
  • Go言語のコンパイラツールチェーンに関するドキュメントや解説記事: Goコンパイラの各コンポーネント(6c, 6g, 6l)の役割を理解するために参照しました。

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

このコミットは、Goコンパイラツールチェーン(6c, 6g, 6l)において、64ビット値から32ビット値への明示的な切り捨て(truncation)を導入するためにMOVQL命令を追加するものです。これにより、コンパイラの最適化フェーズであるコピー伝播(copy propagation)が、意図しない32ビット切り捨てを64ビット値の使用箇所に伝播させ、誤った結果を引き起こす問題を解決します。

コミット

commit e530d6a1e00fbc0149b71bca9f940058838c1c44
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 10 12:51:59 2012 -0400

    6c, 6g, 6l: add MOVQL to make truncation explicit
    
    Without an explicit signal for a truncation, copy propagation
    will sometimes propagate a 32-bit truncation and end up
    overwriting uses of the original 64-bit value.
    
    The case that arose in practice is in C but I believe
    that the same could plausibly happen in Go.
    The main reason we didn't run into the same in Go
    is that I (perhaps incorrectly?) drop MOVL AX, AX
    during gins, so the truncation was never generated, so
    it didn't confuse the optimizer.
    
    Fixes #1315.
    Fixes #3488.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6002043

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

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

元コミット内容

このコミットは、Goコンパイラのバックエンドにおいて、64ビットから32ビットへの値の切り捨てを明示的に示すための新しいアセンブリ命令MOVQLを導入します。これにより、コンパイラの最適化パス、特にコピー伝播が、本来64ビットとして扱われるべき値に対して誤って32ビットの切り捨てを適用してしまう問題を修正します。

具体的には、以下のような状況で問題が発生していました。

  1. 64ビットの値を32ビットの変数に代入する際に、値が切り捨てられる。
  2. コンパイラのコピー伝播最適化が、この32ビットへの切り捨て操作を、元の64ビット値が使用されている他の箇所にまで伝播させてしまう。
  3. 結果として、本来64ビットとして扱われるべき場所で値が誤って切り捨てられ、不正な動作やバグを引き起こす。

この問題はC言語のコードで実際に発生しましたが、Go言語でも同様の状況が発生する可能性が指摘されています。Go言語でこれまでこの問題が顕在化しなかったのは、MOVL AX, AXのような明示的な切り捨て命令がgins(Goの命令生成フェーズ)で削除されていたため、最適化器が混乱する機会がなかったからだと説明されています。

この変更は、Issue #1315とIssue #3488を修正します。

変更の背景

この変更の背景には、コンパイラの最適化、特に「コピー伝播」が引き起こす潜在的なバグがあります。コピー伝播は、ある変数の値が別の変数にコピーされた後、元の変数の代わりにコピー先の変数を使用するようにコードを書き換える最適化手法です。これは通常、パフォーマンス向上に寄与しますが、型変換、特にサイズの異なる型への変換(例: 64ビットから32ビットへの切り捨て)が絡むと、意図しない副作用を生じることがあります。

具体的には、64ビットの整数値を32ビットの整数変数に代入する際、上位32ビットが切り捨てられます。この切り捨て操作が明示的にコンパイラに伝わらない場合、コンパイラは元の64ビット値と切り捨てられた32ビット値を区別できなくなり、最適化の過程で64ビット値が使われるべき場所で誤って32ビットに切り捨てられた値を使ってしまう可能性がありました。

この問題は、Goコンパイラが生成するアセンブリコードにおいて、64ビットから32ビットへの切り捨てが十分に明示されていなかったことに起因します。既存の命令では、この切り捨てが単なるデータ移動と区別されにくく、最適化器が誤った推論を行う余地がありました。

前提知識の解説

コンパイラの最適化

コンパイラの最適化とは、ソースコードを機械語に変換する際に、生成される機械語コードの実行速度やサイズを改善するプロセスです。様々な最適化手法がありますが、このコミットに関連するのは以下の概念です。

  • コピー伝播 (Copy Propagation): ある変数xが別の変数yにコピーされた場合(例: y = x;)、その後のコードでxが使われている箇所をyに置き換える最適化です。これにより、余分なロード/ストア命令を削減し、レジスタの有効活用を促進します。
  • データフロー解析 (Data Flow Analysis): プログラムの実行中にデータがどのように流れるかを分析する技術です。最適化器はこれを用いて、変数の値がどこで定義され、どこで使われているか、その値がプログラムのどの時点で有効であるかなどを判断します。
  • 型システムと型変換 (Type System and Type Conversion): プログラミング言語における型の概念と、ある型から別の型へ値を変換する操作です。Go言語では、異なるサイズの整数型間での代入は、必要に応じて切り捨てや符号拡張が行われます。

アセンブリ言語と命令

アセンブリ言語は、CPUが直接実行できる機械語命令を人間が読める形式で記述したものです。各CPUアーキテクチャには固有の命令セットがあります。このコミットはx86-64アーキテクチャ(64ビットIntel/AMDプロセッサ)を対象としています。

  • MOV命令: データを移動させる基本的な命令です。例えば、MOV AX, BXはBXレジスタの値をAXレジスタにコピーします。
  • レジスタ (Registers): CPU内部にある高速な記憶領域で、演算の対象となるデータを一時的に保持します。x86-64アーキテクチャでは、AX, BX, CX, DXなどの汎用レジスタや、RAX, RBXなどの64ビットレジスタがあります。
  • 切り捨て (Truncation): 広いビット幅の値を狭いビット幅の値に変換する際に、上位ビットを破棄する操作です。例えば、64ビットの値を32ビットに切り捨てると、上位32ビットの情報が失われます。

Goコンパイラツールチェーン

Go言語のコンパイラツールチェーンは、主に以下のコンポーネントで構成されます(このコミット当時の名称)。

  • 6c: The C compiler for amd64 (x86-64) architecture, used for compiling .c files.
  • 6g: The Go compiler for amd64 (x86-64) architecture, used for compiling .go files.
  • 6l: The linker for amd64 (x86-64) architecture, used to link object files into an executable.

これらのツールは、Go言語のクロスコンパイル能力を支えるために、ターゲットアーキテクチャ(この場合はx86-64)に応じてプレフィックス(例: 6はx86-64、8はx86-32、5はARMなど)が付けられています。

技術的詳細

このコミットの核心は、64ビットから32ビットへの明示的な切り捨てを表現する新しいアセンブリ命令MOVQLの導入です。

従来のGoコンパイラでは、64ビット値を32ビット変数に代入する際、例えばAMOVL(Move Long)のような命令が使用されていました。しかし、AMOVLは単に32ビットのデータを移動させる命令であり、それが「64ビット値の切り捨て結果である」というセマンティクスを明示的に持っていませんでした。この曖昧さが、コンパイラの最適化器、特にコピー伝播に誤った推論をさせていました。

最適化器は、あるレジスタに格納された64ビット値が、その後32ビットのレジスタに移動された場合、その32ビット値が元の64ビット値の単なる部分であると解釈し、元の64ビット値が使われるべき場所で、誤って32ビットに切り捨てられた値を伝播させてしまう可能性がありました。これは、特に元の64ビット値がその後も64ビットとして使用される場合に問題となります。

MOVQL命令は、この問題を解決するために導入されました。MOVQLは「Move Quadword to Longword with Truncation」のような意味合いを持ち、64ビットのソースから32ビットのデスティネーションへの移動が、明示的な切り捨て操作であることをコンパイラに伝えます。これにより、最適化器はMOVQL命令を見たときに、その結果が元の64ビット値とは異なる、切り捨てられた32ビット値であることを正確に認識できます。

この変更は、Goコンパイラの以下の部分に影響を与えます。

  1. 命令セットの拡張: src/cmd/6l/6.out.hAMOVQLが新しいアセンブリ命令として定義されます。
  2. リンカの命令テーブル: src/cmd/6l/optab.cAMOVQLのオペコードと処理が追加され、リンカがこの新しい命令を正しく処理できるようになります。
  3. コード生成: src/cmd/6c/txt.csrc/cmd/6g/gsubr.cにおいて、64ビットから32ビットへの型変換が必要な場合に、従来のAMOVLの代わりにAMOVQLが生成されるようになります。これにより、コンパイラが明示的な切り捨て命令を発行するようになります。
  4. 最適化器の認識: src/cmd/6c/peep.c, src/cmd/6c/reg.c, src/cmd/6g/peep.c, src/cmd/6g/reg.cなどの最適化関連のファイルでAMOVQLが認識され、コピー伝播やレジスタ割り当てなどの最適化が、この明示的な切り捨てを考慮して行われるようになります。

この修正により、コンパイラは64ビット値と32ビット切り捨て値のセマンティクスを正確に区別できるようになり、最適化による意図しないバグを防ぐことができます。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/cmd/6l/6.out.h: 新しいアセンブリ命令AMOVQLの定義を追加。

    --- a/src/cmd/6l/6.out.h
    +++ b/src/cmd/6l/6.out.h
    @@ -741,6 +741,8 @@ enum	as
     	APREFETCHT1,
     	APREFETCHT2,
     	APREFETCHNTA,
    +	
    +	AMOVQL,
     
     	ALAST
     };
    
  2. src/cmd/6l/optab.c: リンカの命令テーブルにAMOVQLのエントリを追加。これにより、リンカがAMOVQL命令を認識し、対応する機械語を生成できるようになります。

    --- a/src/cmd/6l/optab.c
    +++ b/src/cmd/6l/optab.c
    @@ -1282,6 +1282,8 @@ Optab optab[] =
     	{ APREFETCHT1,	yprefetch,	Pm,	0x18,(02) },
     	{ APREFETCHT2,	yprefetch,	Pm,	0x18,(03) },
     	{ APREFETCHNTA,	yprefetch,	Pm,	0x18,(00) },
    +	
    +	{ AMOVQL,	yrl_ml,	Px, 0x89 },
     
     	{ AEND },
     	0
    
  3. src/cmd/6g/gsubr.c: Go言語のコード生成部分で、64ビットから32ビットへの切り捨てが必要な場合にAMOVQLを生成するように変更。

    --- a/src/cmd/6g/gsubr.c
    +++ b/src/cmd/6g/gsubr.c
    @@ -706,11 +706,14 @@ gmove(Node *f, Node *t)
     	case CASE(TINT32, TUINT32):
     	case CASE(TUINT32, TINT32):
     	case CASE(TUINT32, TUINT32):
    +		a = AMOVL;
    +		break;
    +
     	case CASE(TINT64, TINT32):	// truncate
     	case CASE(TUINT64, TINT32):
     	case CASE(TINT64, TUINT32):
     	case CASE(TUINT64, TUINT32):
    -		a = AMOVL;
    +		a = AMOVQL;
     		break;
     
     	case CASE(TINT64, TINT64):	// same size
    
  4. src/cmd/6c/txt.c: C言語のコード生成部分で、同様に64ビットから32ビットへの切り捨てにAMOVQLを使用するように変更。多くのCASE文からTIND(ポインタ型)関連の行が削除され、AMOVQLが新しいCASEブロックで導入されています。

    --- a/src/cmd/6c/txt.c
    +++ b/src/cmd/6c/txt.c
    @@ -809,7 +809,6 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TCHAR):
     	case CASE(	TLONG,	TCHAR):
     	case CASE(	TULONG,	TCHAR):
    -	case CASE(	TIND,	TCHAR):
     
     	case CASE(	TCHAR,	TUCHAR):
     	case CASE(	TUCHAR,	TUCHAR):
    @@ -819,7 +818,6 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TUCHAR):
     	case CASE(	TLONG,	TUCHAR):
     	case CASE(	TULONG,	TUCHAR):
    -	case CASE(	TIND,	TUCHAR):
     
     	case CASE(	TSHORT,	TSHORT):
     	case CASE(	TUSHORT,TSHORT):
    @@ -827,7 +825,6 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TSHORT):
     	case CASE(	TLONG,	TSHORT):
     	case CASE(	TULONG,	TSHORT):
    -	case CASE(	TIND,	TSHORT):
     
     	case CASE(	TSHORT,	TUSHORT):
     	case CASE(	TUSHORT,TUSHORT):
    @@ -835,42 +832,26 @@ gmove(Node *f, Node *t)
     	case CASE(	TUINT,	TUSHORT):
     	case CASE(	TLONG,	TUSHORT):
     	case CASE(	TULONG,	TUSHORT):
    -	case CASE(	TIND,	TUSHORT):
     
     	case CASE(	TINT,	TINT):
     	case CASE(	TUINT,	TINT):
     	case CASE(	TLONG,	TINT):
     	case CASE(	TULONG,	TINT):
    -	case CASE(	TIND,	TINT):
     
     	case CASE(	TINT,	TUINT):
     	case CASE(	TUINT,	TUINT):
     	case CASE(	TLONG,	TUINT):
     	case CASE(	TULONG,	TUINT):
    -	case CASE(	TIND,	TUINT):\n-\n-\tcase CASE(\tTUINT,\tTIND):\n-\tcase CASE(\tTVLONG,\tTUINT):\n-\tcase CASE(\tTVLONG,\tTULONG):\n-\tcase CASE(\tTUVLONG, TUINT):\n-\tcase CASE(\tTUVLONG, TULONG):\
      *****/
     	a = AMOVL;
     	break;
     
    -	case CASE(	TVLONG,	TCHAR):\n-	case	CASE(\tTVLONG,\tTSHORT):\n-	case CASE(\tTVLONG,\tTINT):\n-	case CASE(\tTVLONG,\tTLONG):\n-	case CASE(\tTUVLONG, TCHAR):\n-	case	CASE(\tTUVLONG, TSHORT):\n-	case CASE(\tTUVLONG, TINT):\n-	case CASE(\tTUVLONG, TLONG):\
    +	case CASE(	TINT,	TIND):\
     	case CASE(	TINT,	TVLONG):\
     	case CASE(	TINT,	TUVLONG):\
    -	case CASE(	TLONG,	TVLONG):\n-	case CASE(	TINT,	TIND):\
     	case CASE(	TLONG,	TIND):\
    +	case CASE(	TLONG,	TVLONG):\
    +	case CASE(	TLONG,	TUVLONG):\
     	a = AMOVLQSX;
     	if(f->op == OCONST) {
     		f->vconst &= (uvlong)0xffffffffU;
    @@ -886,22 +867,53 @@ gmove(Node *f, Node *t)
     	case CASE(	TULONG,	TVLONG):\
     	case CASE(	TULONG,	TUVLONG):\
     	case CASE(	TULONG,	TIND):\
    -	a = AMOVL;	/* same effect as AMOVLQZX */
    +	a = AMOVLQZX;
     	if(f->op == OCONST) {
     		f->vconst &= (uvlong)0xffffffffU;
     		a = AMOVQ;
     	}
     	break;
    +	
    +	case CASE(	TIND,	TCHAR):\
    +	case CASE(	TIND,	TUCHAR):\
    +	case CASE(	TIND,	TSHORT):\
    +	case CASE(	TIND,	TUSHORT):\
    +	case CASE(	TIND,	TINT):\
    +	case CASE(	TIND,	TUINT):\
    +	case CASE(	TIND,	TLONG):\
    +	case CASE(	TIND,	TULONG):\
    +	case CASE(	TVLONG,	TCHAR):\
    +	case CASE(	TVLONG,	TUCHAR):\
    +	case CASE(	TVLONG,	TSHORT):\
    +	case CASE(	TVLONG,	TUSHORT):\
    +	case CASE(	TVLONG,	TINT):\
    +	case CASE(	TVLONG,	TUINT):\
    +	case CASE(	TVLONG,	TLONG):\
    +	case CASE(	TVLONG,	TULONG):\
    +	case CASE(	TUVLONG,	TCHAR):\
    +	case CASE(	TUVLONG,	TUCHAR):\
    +	case CASE(	TUVLONG,	TSHORT):\
    +	case CASE(	TUVLONG,	TUSHORT):\
    +	case CASE(	TUVLONG,	TINT):\
    +	case CASE(	TUVLONG,	TUINT):\
    +	case CASE(	TUVLONG,	TLONG):\
    +	case CASE(	TUVLONG,	TULONG):\
    +	a = AMOVQL;
    +	if(f->op == OCONST) {
    +		f->vconst &= (int)0xffffffffU;
    +		a = AMOVL;
    +	}
    +	break;	
     
    +	case CASE(	TIND,	TIND):\
     	case CASE(	TIND,	TVLONG):\
    -	case CASE(	TVLONG,	TVLONG):\
    -	case CASE(	TUVLONG,	TVLONG):\
    -	case CASE(	TVLONG,	TUVLONG):\
    -	case CASE(	TUVLONG,	TUVLONG):\
     	case CASE(	TIND,	TUVLONG):\
     	case CASE(	TVLONG,	TIND):\
    +	case CASE(	TVLONG,	TVLONG):\
    +	case CASE(	TVLONG,	TUVLONG):\
     	case CASE(	TUVLONG,	TIND):\
    -	case CASE(	TIND,	TIND):\
    +	case CASE(	TUVLONG,	TVLONG):\
    +	case CASE(	TUVLONG,	TUVLONG):\
     	a = AMOVQ;
     	break;
    
  5. src/cmd/6c/peep.c, src/cmd/6c/reg.c, src/cmd/6g/peep.c, src/cmd/6g/reg.c: これらのファイルは、コンパイラの最適化フェーズ(ピーフホール最適化やレジスタ割り当て)に関連しています。AMOVQLが新しい命令として認識され、最適化のロジックに組み込まれるように変更されています。これにより、最適化器がAMOVQLを正しく扱い、コピー伝播などの最適化が意図しない副作用を引き起こさないようになります。

コアとなるコードの解説

このコミットの主要な目的は、64ビットから32ビットへの切り捨て操作をコンパイラが明示的に認識できるようにすることです。これを実現するために、新しいアセンブリ命令AMOVQLが導入され、Goコンパイラのコード生成および最適化パスに統合されました。

  • src/cmd/6l/6.out.hsrc/cmd/6l/optab.c: これらはリンカに関連するファイルで、新しい命令AMOVQLをGoのアセンブリ言語の命令セットに追加し、リンカがその命令を機械語に変換できるようにします。6.out.hは命令の列挙型を定義し、optab.cは各命令に対応するオペコード(機械語のバイト列)と処理関数を定義するテーブルです。AMOVQLがこのテーブルに追加されることで、リンカはAMOVQLを正しく解釈し、実行可能なバイナリに含めることができるようになります。

  • src/cmd/6g/gsubr.csrc/cmd/6c/txt.c: これらのファイルは、Goコンパイラ(6g)とCコンパイラ(6c)のコード生成部分です。gmove関数は、異なる型の間の値の移動(代入)を処理し、適切なアセンブリ命令を生成します。 変更前は、64ビットから32ビットへの切り捨てを伴う代入(例: TINT64からTINT32への変換)に対してAMOVL(Move Long)が生成されていました。しかし、AMOVLは単なる32ビットのデータ移動であり、切り捨てのセマンティクスを明示的に持ちません。 変更後は、これらのケースでAMOVQLが生成されるようになります。AMOVQLは、64ビット値を32ビットに切り捨てるという操作を明示的に示すため、コンパイラの最適化器がこの操作を正確に理解できるようになります。 src/cmd/6c/txt.cでは、TIND(ポインタ型)から整数型への変換に関する多くのCASE文が削除され、TVLONG(64ビット整数)やTUVLONG(符号なし64ビット整数)からより小さい整数型への変換、およびTINDから整数型への変換に対してAMOVQLが導入されています。これは、ポインタが64ビットであるシステムにおいて、ポインタ値を整数型に変換する際に切り捨てが発生しうるため、その場合も明示的なMOVQLを使用するようにしたものです。

  • src/cmd/6c/peep.c, src/cmd/6c/reg.c, src/cmd/6g/peep.c, src/cmd/6g/reg.c: これらのファイルは、コンパイラの最適化フェーズ(ピーフホール最適化やレジスタ割り当て)に関連しています。ピーフホール最適化は、生成されたアセンブリコードの小さなパターンをより効率的なコードに置き換えるものです。レジスタ割り当ては、プログラムの変数をCPUのレジスタに効率的に割り当てることで、メモリアクセスを減らし、実行速度を向上させます。 これらの最適化器は、プログラムのデータフローを分析し、コピー伝播などの最適化を適用します。AMOVQLがこれらのファイルで認識されるように変更されたことで、最適化器はAMOVQLが明示的な切り捨て操作であることを理解し、その結果として生成される32ビット値が、元の64ビット値とは異なるセマンティクスを持つことを考慮するようになります。これにより、最適化器が誤って64ビット値の代わりに切り捨てられた32ビット値を伝播させることを防ぎ、バグの発生を抑制します。

この一連の変更により、Goコンパイラは64ビットから32ビットへの型変換をより正確に扱い、最適化による潜在的なバグを回避できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のIssueトラッカー (GitHub): 上記のIssueリンクから詳細な議論や背景情報を参照しました。
  • Go言語のソースコード: コミットに含まれるファイルパスから、Goコンパイラの内部構造と各ファイルの役割を理解しました。
  • コンパイラ最適化に関する一般的な知識: コピー伝播、データフロー解析、ピーフホール最適化などの概念は、コンパイラ理論の一般的な知識に基づいています。
  • x86-64アセンブリ言語の知識: MOV命令やレジスタの概念は、x86-64アセンブリ言語の一般的な知識に基づいています。
  • Go言語のコンパイラツールチェーンに関するドキュメントや解説記事: Goコンパイラの各コンポーネント(6c, 6g, 6l)の役割を理解するために参照しました。