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

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

このコミットは、Goコンパイラのバックエンドにおける2つの異なる問題を修正しています。具体的には、src/cmd/6g/gsubr.csrc/cmd/gc/subr.c の2つのファイルが変更されています。

src/cmd/6g/gsubr.c は、Goコンパイラのamd64アーキテクチャ向けコード生成部分に関連しており、定数(immediate constants)のメモリへの移動に関する最適化とバグ修正を含んでいます。

src/cmd/gc/subr.c は、Goコンパイラの共通バックエンド部分であり、型の等価性チェックを行う eqtype 関数における無限再帰の可能性を修正しています。

コミット

fix possible infinite recursion in eqtype.

don't use intermediate register to move
32-or-fewer-bit immediate constants
into memory.

R=ken
OCL=23726
CL=23726

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

https://github.com/golang/go/commit/282bf8cc8c36ebbf02c1ee524daff2ef12b531f1

元コミット内容

commit 282bf8cc8c36ebbf02c1ee524daff2ef12b531f1
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jan 28 16:42:26 2009 -0800

    fix possible infinite recursion in eqtype.
    
    don't use intermediate register to move
    32-or-fewer-bit immediate constants
    into memory.
    
    R=ken
    OCL=23726
    CL=23726
---
 src/cmd/6g/gsubr.c | 4 +++-
 src/cmd/gc/subr.c  | 4 ++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/cmd/6g/gsubr.c b/src/cmd/6g/gsubr.c
index 6934c6f30c..86ba52c3fe 100644
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -554,7 +554,9 @@ gmove(Node *f, Node *t)
 		goto st;
 
 	st:
-		if(f->op == OCONST) {
+		// 64-bit immediates only allowed for move into registers.
+		// this is not a move into a register.
+		if(f->op == OCONST || (f->op == OLITERAL && !t64)) {
 			gins(a, f, t);
 			return;
 		}
diff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c
index 870a90167a..6cd0384126 100644
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1620,7 +1620,7 @@ signame(Type *t)
 	// so that it can be referred to by the runtime.
 	if(strcmp(buf, "interface { }") == 0)
 		strcpy(buf, "empty");
-	
+
 	// special case: sigi.... is just too hard to read in assembly.
 	if(strcmp(buf, "...") == 0)
 		strcpy(buf, "dotdotdot");
@@ -1707,7 +1707,7 @@ eqtype(Type *t1, Type *t2, int d)
 					return 0;
 				if(ta->etype != TFIELD || tb->etype != TFIELD)
 					return 0;
-				if(!eqtype(ta->type, tb->type, 0))
+				if(!eqtype(ta->type, tb->type, d+1))
 					return 0;
 				ta = ta->down;
 				tb = tb->down;

変更の背景

このコミットは、Goコンパイラの2つの異なる領域における問題を解決するために行われました。

  1. eqtype 関数における無限再帰の修正: Goコンパイラは、プログラム内の型の等価性を判断するために eqtype 関数を使用します。型定義が再帰的である場合(例えば、構造体が自分自身へのポインタを持つ場合など)、この関数が無限に再帰してしまう可能性がありました。これはコンパイラのクラッシュやハングアップを引き起こす重大なバグです。この修正は、再帰の深さを追跡するメカニズムを導入することで、この問題を解決しようとしています。

  2. 32ビット以下の即値定数のメモリへの移動の最適化: Goコンパイラは、ソースコード中の定数値を機械語に変換する際に、それらをレジスタやメモリに配置します。x86-64アーキテクチャでは、32ビット以下の即値定数をメモリに直接移動する際に、中間レジスタを使用する必要がない場合があります。中間レジスタを使用すると、不要な命令が生成され、コードの効率が低下します。この修正は、このような場合に中間レジスタの使用を避け、より効率的なコードを生成することを目的としています。

前提知識の解説

Goコンパイラ (gc, 6g)

  • gc (Go Compiler): Go言語の公式コンパイラ群の総称、またはその共通バックエンド部分を指します。Goコンパイラは、Goのソースコードを各ターゲットアーキテクチャの機械語に変換する役割を担います。
  • 6g: Goコンパイラの一部で、x86-64 (amd64) アーキテクチャ向けのコード生成を担当するコンポーネントです。Goの初期のコンパイラは、ターゲットアーキテクチャごとに 5g (ARM), 6g (amd64), 8g (x86) のように命名されていました。

型システムと eqtype

  • 型 (Type): プログラミング言語において、値の種類を定義するものです。Go言語では、int, string, struct, interface など、様々な型があります。
  • 型の等価性 (Type Equality): 2つの型が同じであると見なされるかどうかを判断する概念です。Go言語では、構造体のフィールドの型や順序、インターフェースのメソッドシグネチャなどが型の等価性に影響します。
  • eqtype 関数: Goコンパイラ内部で使用される関数で、2つのGoの型が等しいかどうかを再帰的に比較します。この関数は、型チェック、インターフェースの実装チェック、型推論など、コンパイラの多くの部分で利用されます。再帰的な型(例えば、リンクリストのノードのように、自分自身へのポインタを持つ構造体)を比較する際には、無限ループに陥らないように注意が必要です。

アセンブリとレジスタ、即値定数

  • アセンブリ言語 (Assembly Language): 機械語と1対1に対応する低レベルのプログラミング言語です。コンパイラは、Goのコードを最終的にアセンブリ言語(または直接機械語)に変換します。
  • レジスタ (Register): CPU内部にある高速な記憶領域です。データや命令のアドレスを一時的に保持するために使用されます。レジスタへのアクセスはメモリへのアクセスよりもはるかに高速です。
  • 即値定数 (Immediate Constant): 命令の一部として直接エンコードされる定数値です。例えば、MOV EAX, 5 という命令では、5 が即値定数です。
  • gmove 関数: Goコンパイラのコード生成フェーズで使用される関数で、ある場所(レジスタ、メモリ、定数など)から別の場所へデータを移動するためのアセンブリ命令を生成します。
  • OCONST / OLITERAL: GoコンパイラのAST (Abstract Syntax Tree) ノードの種類を表す内部定数です。OCONST はコンパイル時に値が確定する定数を、OLITERAL はリテラル(ソースコードに直接記述された値)を表します。
  • t64: ターゲットアーキテクチャが64ビットであるかどうかを示すフラグまたは変数。

技術的詳細

eqtype 関数における無限再帰の修正 (src/cmd/gc/subr.c)

元の eqtype 関数は、構造体や配列などの複合型を比較する際に、その要素の型を再帰的に比較していました。特に、TFIELD (構造体のフィールド) の比較において、eqtype(ta->type, tb->type, 0) のように、再帰呼び出しの際に深さを示す引数 d を常に 0 にリセットしていました。

// 変更前
if(!eqtype(ta->type, tb->type, 0))

この d 引数は、再帰の深さを追跡し、無限再帰を検出するためのものです。しかし、常に 0 にリセットされるため、再帰的な型定義(例: type Node struct { Next *Node })を比較する際に、コンパイラは同じ型の比較を無限に繰り返してしまう可能性がありました。

修正では、この d 引数を d+1 とすることで、再帰の深さを正しくインクリメントするように変更されました。

// 変更後
if(!eqtype(ta->type, tb->type, d+1))

これにより、eqtype 関数は再帰の深さを認識し、ある一定の深さを超えた場合や、既に比較中の型ペアが再度現れた場合に、無限再帰を回避できるようになります。これは、コンパイラが再帰的な型定義を正しく処理し、クラッシュを防ぐために不可欠な変更です。

32ビット以下の即値定数のメモリへの移動の最適化 (src/cmd/6g/gsubr.c)

gmove 関数は、Goコンパイラがアセンブリコードを生成する際に、データの移動命令を生成する役割を担っています。このコミットの変更は、特にamd64アーキテクチャにおいて、32ビット以下の即値定数をメモリに移動する際の効率を改善します。

元のコードでは、f->op == OCONST の場合に gins(a, f, t) を呼び出していましたが、これは64ビットの即値定数も含まれる可能性がありました。x86-64アーキテクチャでは、64ビットの即値定数を直接メモリに移動する命令は存在せず、通常はまずレジスタにロードしてからメモリに移動する必要があります。しかし、32ビット以下の即値定数であれば、多くの場合、中間レジスタを介さずに直接メモリに移動する命令(例: MOV DWORD PTR [mem_loc], immediate_value)が存在し、より効率的です。

変更後のコードでは、条件が f->op == OCONST || (f->op == OLITERAL && !t64) に拡張されました。

// 変更前
if(f->op == OCONST) {

// 変更後
if(f->op == OCONST || (f->op == OLITERAL && !t64)) {

この変更の意図は、コメントに「64-bit immediates only allowed for move into registers. this is not a move into a register.」とあるように、64ビットの即値定数はレジスタへの移動のみが許容され、メモリへの直接移動はできないというアーキテクチャの制約を考慮しています。

  • f->op == OCONST: これは引き続き、一般的な定数(即値定数を含む)のケースをカバーします。
  • (f->op == OLITERAL && !t64): これは、リテラルであり、かつターゲットが64ビットアーキテクチャではない(つまり32ビット以下)の場合を明示的に追加しています。これにより、32ビット以下の即値定数をメモリに移動する際に、中間レジスタを介さずに直接移動する命令を生成できるようになります。

この最適化により、生成されるアセンブリコードの命令数が減少し、実行効率が向上します。特に、多数の小さな定数がメモリに格納されるようなシナリオで効果を発揮します。

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

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

--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -554,7 +554,9 @@ gmove(Node *f, Node *t)
 		goto st;
 
 	st:
-		if(f->op == OCONST) {
+		// 64-bit immediates only allowed for move into registers.
+		// this is not a move into a register.
+		if(f->op == OCONST || (f->op == OLITERAL && !t64)) {
 			gins(a, f, t);
 			return;
 		}

src/cmd/gc/subr.c の変更

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1707,7 +1707,7 @@ eqtype(Type *t1, Type *t2, int d)
 					return 0;
 				if(ta->etype != TFIELD || tb->etype != TFIELD)
 					return 0;
-				if(!eqtype(ta->type, tb->type, 0))
+				if(!eqtype(ta->type, tb->type, d+1))
 					return 0;
 				ta = ta->down;
 				tb = tb->down;

コアとなるコードの解説

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

gmove 関数内の if 文の条件が変更されました。

  • 変更前: if(f->op == OCONST)
    • これは、ソースオペランド f が定数である場合に、gins 関数(アセンブリ命令を生成する関数)を呼び出して、その定数をターゲット t に移動する命令を生成していました。
  • 変更後: if(f->op == OCONST || (f->op == OLITERAL && !t64))
    • この新しい条件は、以下のいずれかの場合に gins を呼び出します。
      1. f が一般的な定数 (OCONST) である場合。
      2. f がリテラル (OLITERAL) であり、かつターゲットアーキテクチャが64ビットではない (!t64) 場合。
    • この追加された条件 (f->op == OLITERAL && !t64) は、特に32ビット以下のリテラル定数をメモリに直接移動する際に、中間レジスタを介さない効率的な命令を生成するためのものです。これにより、不要なレジスタ操作を削減し、生成されるコードのパフォーマンスを向上させます。

src/cmd/gc/subr.c の変更解説

eqtype 関数内の再帰呼び出しの引数が変更されました。

  • 変更前: if(!eqtype(ta->type, tb->type, 0))
    • eqtype が再帰的に呼び出される際に、深さを示す引数 d が常に 0 にリセットされていました。これは、再帰的な型定義を比較する際に、無限ループに陥る可能性がありました。
  • 変更後: if(!eqtype(ta->type, tb->type, d+1))
    • 再帰呼び出しの際に、深さを示す引数 dd+1 とインクリメントされるようになりました。これにより、eqtype 関数は再帰の深さを正しく追跡し、無限再帰を検出して回避できるようになります。これは、コンパイラの安定性と正確性を保証するために非常に重要です。

関連リンク

  • Go言語のコンパイラに関するドキュメントやソースコードリポジトリ
  • x86-64アセンブリ言語の命令セットリファレンス(特にMOV命令と即値定数に関する部分)
  • Go言語の型システムに関する公式ドキュメント

参考にした情報源リンク

  • Go言語の公式ドキュメント: https://go.dev/doc/
  • Goコンパイラのソースコード: https://github.com/golang/go
  • x86-64 Instruction Set Reference Manuals (Intel/AMD)
  • Goコンパイラの内部構造に関するブログ記事や論文(一般的な情報源)