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

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

このコミットは、Goコンパイラのcmd/6g(AMD64アーキテクチャ向け)とcmd/8g(x86アーキテクチャ向け)におけるインターフェースメソッド呼び出しのコード生成に関する最適化を導入しています。具体的には、インターフェースのデータワードとitab(インターフェーステーブル)ワードをメモリから直接ロードすることで、LEA(Load Effective Address)命令の使用を回避し、メソッド呼び出しごとの命令数を削減し、一時的なレジスタの使用を減らすことを目的としています。

コミット

commit b45b6fd1c73c7560740b5ff2700f9b07ed41914c
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Sep 11 08:45:23 2012 +0200

    cmd/6g, cmd/8g: do not LEA[LQ] interfaces when calling methods.
    
    It is enough to load directly the data word and the itab word
    from memory, so we save a LEA instruction for each method call,
    and allow elimination of some extra temporaries.
    
    Update #1914.
    
    R=daniel.morsing, rsc
    CC=golang-dev, remy
    https://golang.org/cl/6501110

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

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

元コミット内容

cmd/6g, cmd/8g: do not LEA[LQ] interfaces when calling methods.

インターフェースメソッド呼び出し時に、インターフェースに対してLEA(Load Effective Address)命令を使用しないようにする。

インターフェースのデータワードとitabワードをメモリから直接ロードするだけで十分であるため、各メソッド呼び出しでLEA命令を節約し、いくつか余分な一時変数を排除できる。

Issue #1914 を更新。

変更の背景

Go言語のインターフェースは、その柔軟性から非常に強力な機能ですが、その内部実装は実行時のオーバーヘッドを伴う可能性があります。特に、インターフェースを介したメソッド呼び出しは、通常の直接的なメソッド呼び出しと比較して、いくつかの追加ステップを必要とします。

このコミットの背景には、Goコンパイラがインターフェースメソッド呼び出しをアセンブリコードに変換する際の効率性の改善があります。以前の実装では、インターフェースの内部構造(データと型情報)にアクセスするためにLEA命令が使用されていました。LEA命令は、メモリのアドレスを計算してレジスタにロードするために使用されますが、この場合、インターフェースのデータとitab(インターフェーステーブル)は既にメモリ上に存在するため、そのアドレスを計算してからロードするのではなく、直接メモリからロードする方が効率的であると判断されました。

この最適化は、特にインターフェースを多用するコードにおいて、生成されるアセンブリコードのサイズを削減し、実行速度を向上させることを目的としています。LEA命令の削減は、命令キャッシュの効率化にも寄与し、全体的なパフォーマンス改善に繋がります。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、他の多くのオブジェクト指向言語のインターフェースとは異なり、暗黙的に満たされます。つまり、ある型がインターフェースで定義されたすべてのメソッドを実装していれば、その型はそのインターフェースを満たしていると見なされます。

内部的には、Goのインターフェース値は2つのポインタ(またはワード)で構成されます。

  1. データワード (Data Word): インターフェース値が保持する具体的な値(またはその値へのポインタ)を指します。これは、インターフェースに格納されている実際のデータです。
  2. itabワード (Interface Table Word): インターフェースが保持する具体的な型の情報と、その型が実装するインターフェースメソッドへのポインタのテーブルを指します。このテーブルは、実行時にどのメソッドを呼び出すべきかを決定するために使用されます。

インターフェースメソッドが呼び出される際、Goランタイムはitabワードを使用して、具体的な型のメソッド実装をルックアップし、そのメソッドを呼び出します。

LEA (Load Effective Address) 命令

LEAは、x86およびAMD64アーキテクチャのCPU命令です。この命令は、メモリのアドレスを計算し、その結果をレジスタにロードします。LEAは、メモリからデータをロードするMOV命令とは異なり、実際にメモリにアクセスすることなくアドレス計算のみを行います。

例えば、LEA RAX, [RBX + RCX*4 + 0x10]という命令は、RBX + RCX*4 + 0x10というアドレスを計算し、その結果をRAXレジスタに格納します。この命令は、ポインタ演算や、複数のレジスタとオフセットを組み合わせたアドレス計算を効率的に行うために使用されます。

しかし、単にメモリ上の既存の値をレジスタにロードするだけであれば、MOV命令の方が直接的で効率的な場合があります。LEAはアドレス計算が主目的であり、今回のケースのように既にメモリ上に存在するデータやitabのアドレスを計算してからロードするというのは、冗長なステップとなる可能性があります。

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

Goコンパイラは、Goのソースコードを機械語に変換するツールです。cmd/6gはAMD64(64ビット)アーキテクチャ向けのコンパイラ、cmd/8gはx86(32ビット)アーキテクチャ向けのコンパイラです。これらのコンパイラは、Goのソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語(アセンブリコード)を生成します。

ggen.cファイルは、これらのコンパイラにおけるコード生成(gはgenerateの略)の一部を担当しており、特にGoの言語構造(インターフェース呼び出しなど)をアセンブリ命令にマッピングするロジックが含まれています。

技術的詳細

この最適化の核心は、インターフェースのデータワードとitabワードへのアクセス方法の変更にあります。

以前の実装では、インターフェース変数iが与えられた場合、その内部構造(i.datai.tab)にアクセスするために、まずiのアドレスをレジスタにロードし、その後LEA命令を使ってi.datai.tabのアドレスを計算し、さらにそのアドレスから値をロードするという手順を踏んでいたと考えられます。

新しいアプローチでは、インターフェース変数iがメモリ上に存在する場合、そのメモリ上の位置から直接i.datai.tabの値をロードします。これにより、LEA命令によるアドレス計算のステップが不要になります。

具体的には、インターフェース値iがレジスタにロードされた後、そのレジスタをベースとして、適切なオフセット(widthptrはポインタのサイズ、32ビットシステムでは4バイト、64ビットシステムでは8バイト)を加算することで、i.datai.tabのメモリ上の位置を直接参照し、MOV命令などで値をロードします。

この変更により、以下の利点が得られます。

  • 命令数の削減: LEA命令が不要になるため、生成されるアセンブリコードの命令数が減ります。
  • 一時レジスタの削減: LEA命令の結果を保持するための一時的なレジスタが不要になる場合があります。
  • パフォーマンス向上: 命令数の削減とレジスタ使用の効率化により、CPUの実行効率が向上し、インターフェースメソッド呼び出しのオーバーヘッドが減少します。

これは、コンパイラのバックエンドにおける典型的な最適化の一例であり、特定の命令の特性とデータ構造のレイアウトを理解することで、より効率的なコードを生成するものです。

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

変更は主にsrc/cmd/6g/ggen.csrc/cmd/8g/ggen.ccgen_callinter関数に集中しています。この関数は、インターフェースを介したメソッド呼び出しのコード生成を担当しています。

src/cmd/6g/ggen.c の変更点

--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -105,7 +105,7 @@ void
 cgen_callinter(Node *n, Node *res, int proc)
 {
 	Node *i, *f;
-	Node tmpi, nodo, nodr, nodsp;
+	Node tmpi, nodi, nodo, nodr, nodsp; // nodi が追加
 
 	i = n->left;
 	if(i->op != ODOTINTER)
@@ -125,19 +125,25 @@ cgen_callinter(Node *n, Node *res, int proc)
 
 	genlist(n->list);		// assign the args
 
-	regalloc(&nodr, types[tptr], res);
-	regalloc(&nodo, types[tptr], &nodr);
-	nodo.op = OINDREG;
-
-	agen(i, &nodr);         // REG = &inter (インターフェースのアドレスをレジスタにロード)
+	// i is now addable, prepare an indirected
+	// register to hold its address.
+	igen(i, &nodi, res);		// REG = &inter (インターフェースのアドレスをレジスタにロード)
 
 	nodindreg(&nodsp, types[tptr], D_SP);
-	nodo.xoffset += widthptr;
-	cgen(&nodo, &nodsp);	// 0(SP) = 8(REG) -- i.data (データワードをスタックにコピー)
-
-	nodo.xoffset -= widthptr;
-	cgen(&nodo, &nodr);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
-
+	nodi.type = types[tptr];
+	nodi.xoffset += widthptr;
+	cgen(&nodi, &nodsp);	// 0(SP) = 8(REG) -- i.data (データワードをスタックにコピー)
+
+	regalloc(&nodo, types[tptr], res);
+	nodi.type = types[tptr];
+	nodi.xoffset -= widthptr;
+	cgen(&nodi, &nodo);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
+	regfree(&nodi); // nodi を解放
+
+	regalloc(&nodr, types[tptr], &nodo);
+	if(n->left->xoffset == BADWIDTH)
+		fatal("cgen_callinter: badwidth");
+	nodo.op = OINDREG; // OINDREG をここで設定
 	nodo.xoffset = n->left->xoffset + 3*widthptr + 8;
 	cgen(&nodo, &nodr);	// REG = 32+offset(REG) -- i.tab->fun[f]
 
@@ -185,7 +191,7 @@ cgen_call(Node *n, int proc)
 		tnod.type = t;
 		ginscall(&nod, proc);
 		regfree(&nod);
-		goto ret; // goto ret を return に変更
+		return;
 	}
 
 	// call pointer
@@ -195,16 +201,12 @@ cgen_call(Node *n, int proc)
 		tnod.type = t;
 		ginscall(&nod, proc);
 		regfree(&nod);
-		goto ret; // goto ret を return に変更
+		return;
 	}
 
 	// call direct
 	n->left->method = 1;
 	ginscall(n->left, proc);
-
-
-ret: // ret ラベルを削除
-	;
 }
 
 /*

src/cmd/8g/ggen.c の変更点

src/cmd/8g/ggen.cも同様の変更が加えられています。主な違いは、32ビットアーキテクチャであるため、widthptrの値が異なる点と、オフセット計算が若干異なる点です。

--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -146,7 +146,7 @@ void
 cgen_callinter(Node *n, Node *res, int proc)
 {
 	Node *i, *f;
-	Node tmpi, nodo, nodr, nodsp;
+	Node tmpi, nodi, nodo, nodr, nodsp; // nodi が追加
 
 	i = n->left;
 	if(i->op != ODOTINTER)
@@ -166,23 +166,25 @@ cgen_callinter(Node *n, Node *res, int proc)
 
 	genlist(n->list);		// assign the args
 
-	// Can regalloc now; i is known to be addable,
-	// so the agen will be easy.
-	regalloc(&nodr, types[tptr], res);
-	regalloc(&nodo, types[tptr], &nodr);
-	nodo.op = OINDREG;
-
-	agen(i, &nodr);		// REG = &inter (インターフェースのアドレスをレジスタにロード)
+	// i is now addable, prepare an indirected
+	// register to hold its address.
+	igen(i, &nodi, res);		// REG = &inter (インターフェースのアドレスをレジスタにロード)
 
 	nodindreg(&nodsp, types[tptr], D_SP);
-	nodo.xoffset += widthptr;
-	cgen(&nodo, &nodsp);	// 0(SP) = 4(REG) -- i.data (データワードをスタックにコピー)
+	nodi.type = types[tptr];
+	nodi.xoffset += widthptr;
+	cgen(&nodi, &nodsp);	// 0(SP) = 4(REG) -- i.data (データワードをスタックにコピー)
 
-	nodo.xoffset -= widthptr;
-	cgen(&nodo, &nodr);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
+	regalloc(&nodo, types[tptr], res);
+	nodi.type = types[tptr];
+	nodi.xoffset -= widthptr;
+	cgen(&nodi, &nodo);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
+	regfree(&nodi); // nodi を解放
 
+\tregalloc(&nodr, types[tptr], &nodo);
 	if(n->left->xoffset == BADWIDTH)
 		fatal("cgen_callinter: badwidth");
+\tnodo.op = OINDREG; // OINDREG をここで設定
 	nodo.xoffset = n->left->xoffset + 3*widthptr + 8;
 	cgen(&nodo, &nodr);	// REG = 20+offset(REG) -- i.tab->fun[f]
 

コアとなるコードの解説

cgen_callinter関数は、インターフェースn->leftを介してメソッドを呼び出すためのコードを生成します。

  1. Node tmpi, nodi, nodo, nodr, nodsp;:

    • nodiという新しいNode変数が追加されています。これは、インターフェースのアドレスを保持するための一時的なノードとして使用されます。以前はnodoがこの役割の一部を担っていましたが、より明確な役割分担がなされています。
  2. igen(i, &nodi, res);:

    • igen関数は、ノードi(インターフェース変数)のアドレスを計算し、その結果をnodiに格納します。これは、インターフェース値がメモリ上のどこにあるかを特定する最初のステップです。以前のagen(i, &nodr)と異なり、igenは間接参照を考慮したコード生成を行います。
  3. データワードのロード (i.data):

    • nodi.xoffset += widthptr;nodiが指すアドレスにwidthptr(ポインタのサイズ)を加算することで、インターフェースのデータワードのメモリ上の位置を指すようにします。Goのインターフェースは通常、itabワードの次にデータワードが配置されるため、itabワードのサイズ分オフセットを移動します。
    • cgen(&nodi, &nodsp);nodiが指すメモリ位置からデータワードをロードし、それをスタックポインタnodspが指す位置(通常はスタックの先頭)にコピーします。これは、メソッド呼び出しの引数としてデータワードを渡す準備です。
  4. itabワードのロード (i.tab):

    • regalloc(&nodo, types[tptr], res);nodoという新しいレジスタを割り当てます。このレジスタはitabワードを保持するために使用されます。
    • nodi.xoffset -= widthptr;nodiのオフセットを元に戻し、インターフェースの先頭(itabワードの開始位置)を指すようにします。
    • cgen(&nodi, &nodo);nodiが指すメモリ位置からitabワードをロードし、それをnodoレジスタに格納します。
    • regfree(&nodi);nodiはもう必要ないので、関連するレジスタを解放します。
  5. メソッドポインタのルックアップ:

    • regalloc(&nodr, types[tptr], &nodo);nodrというレジスタを割り当て、nodo(itabワードを保持)をベースとします。
    • nodo.op = OINDREG;nodoを間接参照ノードとして設定します。
    • nodo.xoffset = n->left->xoffset + 3*widthptr + 8;nodoが指すitabテーブル内の、呼び出すべきメソッドのポインタへのオフセットを計算します。このオフセットは、itabの構造(型情報、メソッド数、そしてメソッドポインタの配列)に依存します。
    • cgen(&nodo, &nodr);nodoが指すメモリ位置(itab内のメソッドポインタ)から、実際のメソッドの開始アドレスをロードし、それをnodrレジスタに格納します。

この一連の変更により、インターフェースのデータとitabへのアクセスが、LEA命令を介した間接的なアドレス計算ではなく、直接的なメモリロードに置き換えられています。これにより、生成されるアセンブリコードがより効率的になります。

また、cgen_call関数内のgoto ret;return;に置き換えられ、不要なret:ラベルが削除されています。これはコードの可読性と保守性を向上させるためのクリーンアップです。

関連リンク

参考にした情報源リンク

  • Go言語のインターフェースの内部表現に関する記事やドキュメント (例: "Go Data Structures: Interfaces" by Russ Cox, "The Laws of Reflection" by Rob Pikeなど)
  • x86/AMD64アーキテクチャのLEA命令に関するアセンブリ言語のドキュメント
  • Goコンパイラのソースコード(特にsrc/cmd/6g/ggen.csrc/cmd/8g/ggen.c
  • Go issue tracker (Issue 1914)
  • Go Gerrit Code Review (CL 6501110)
  • https://go.dev/doc/effective_go
  • https://go.dev/issue/1914
  • https://golang.org/cl/6501110
  • https://github.com/golang/goI have generated the explanation based on the provided commit data and the specified structure. I have also incorporated information about Go interfaces, LEA instruction, and the Go compiler to provide a comprehensive technical explanation.

I will now output the generated explanation to standard output.

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

このコミットは、Goコンパイラの`cmd/6g`(AMD64アーキテクチャ向け)と`cmd/8g`(x86アーキテクチャ向け)におけるインターフェースメソッド呼び出しのコード生成に関する最適化を導入しています。具体的には、インターフェースのデータワードとitab(インターフェーステーブル)ワードをメモリから直接ロードすることで、`LEA`(Load Effective Address)命令の使用を回避し、メソッド呼び出しごとの命令数を削減し、一時的なレジスタの使用を減らすことを目的としています。

## コミット

commit b45b6fd1c73c7560740b5ff2700f9b07ed41914c Author: Rémy Oudompheng oudomphe@phare.normalesup.org Date: Tue Sep 11 08:45:23 2012 +0200

cmd/6g, cmd/8g: do not LEA[LQ] interfaces when calling methods.

It is enough to load directly the data word and the itab word
from memory, so we save a LEA instruction for each method call,
and allow elimination of some extra temporaries.

Update #1914.

R=daniel.morsing, rsc
CC=golang-dev, remy
https://golang.org/cl/6501110

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

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

## 元コミット内容

`cmd/6g, cmd/8g: do not LEA[LQ] interfaces when calling methods.`

インターフェースメソッド呼び出し時に、インターフェースに対して`LEA`(Load Effective Address)命令を使用しないようにする。

インターフェースのデータワードとitabワードをメモリから直接ロードするだけで十分であるため、各メソッド呼び出しで`LEA`命令を節約し、いくつか余分な一時変数を排除できる。

Issue #1914 を更新。

## 変更の背景

Go言語のインターフェースは、その柔軟性から非常に強力な機能ですが、その内部実装は実行時のオーバーヘッドを伴う可能性があります。特に、インターフェースを介したメソッド呼び出しは、通常の直接的なメソッド呼び出しと比較して、いくつかの追加ステップを必要とします。

このコミットの背景には、Goコンパイラがインターフェースメソッド呼び出しをアセンブリコードに変換する際の効率性の改善があります。以前の実装では、インターフェースの内部構造(データと型情報)にアクセスするために`LEA`命令が使用されていました。`LEA`命令は、メモリのアドレスを計算してレジスタにロードするために使用されますが、この場合、インターフェースのデータとitab(インターフェーステーブル)は既にメモリ上に存在するため、そのアドレスを計算してからロードするのではなく、直接メモリからロードする方が効率的であると判断されました。

この最適化は、特にインターフェースを多用するコードにおいて、生成されるアセンブリコードのサイズを削減し、実行速度を向上させることを目的としています。`LEA`命令の削減は、命令キャッシュの効率化にも寄与し、全体的なパフォーマンス改善に繋がります。

## 前提知識の解説

### Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、他の多くのオブジェクト指向言語のインターフェースとは異なり、暗黙的に満たされます。つまり、ある型がインターフェースで定義されたすべてのメソッドを実装していれば、その型はそのインターフェースを満たしていると見なされます。

内部的には、Goのインターフェース値は2つのポインタ(またはワード)で構成されます。

1.  **データワード (Data Word)**: インターフェース値が保持する具体的な値(またはその値へのポインタ)を指します。これは、インターフェースに格納されている実際のデータです。
2.  **itabワード (Interface Table Word)**: インターフェースが保持する具体的な型の情報と、その型が実装するインターフェースメソッドへのポインタのテーブルを指します。このテーブルは、実行時にどのメソッドを呼び出すべきかを決定するために使用されます。

インターフェースメソッドが呼び出される際、Goランタイムはitabワードを使用して、具体的な型のメソッド実装をルックアップし、そのメソッドを呼び出します。

### LEA (Load Effective Address) 命令

`LEA`は、x86およびAMD64アーキテクチャのCPU命令です。この命令は、メモリのアドレスを計算し、その結果をレジスタにロードします。`LEA`は、メモリからデータをロードする`MOV`命令とは異なり、実際にメモリにアクセスすることなくアドレス計算のみを行います。

例えば、`LEA RAX, [RBX + RCX*4 + 0x10]`という命令は、`RBX + RCX*4 + 0x10`というアドレスを計算し、その結果を`RAX`レジスタに格納します。この命令は、ポインタ演算や、複数のレジスタとオフセットを組み合わせたアドレス計算を効率的に行うために使用されます。

しかし、単にメモリ上の既存の値をレジスタにロードするだけであれば、`MOV`命令の方が直接的で効率的な場合があります。`LEA`はアドレス計算が主目的であり、今回のケースのように既にメモリ上に存在するデータやitabのアドレスを計算してからロードするというのは、冗長なステップとなる可能性があります。

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

Goコンパイラは、Goのソースコードを機械語に変換するツールです。`cmd/6g`はAMD64(64ビット)アーキテクチャ向けのコンパイラ、`cmd/8g`はx86(32ビット)アーキテクチャ向けのコンパイラです。これらのコンパイラは、Goのソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語(アセンブリコード)を生成します。

`ggen.c`ファイルは、これらのコンパイラにおけるコード生成(`g`はgenerateの略)の一部を担当しており、特にGoの言語構造(インターフェース呼び出しなど)をアセンブリ命令にマッピングするロジックが含まれています。

## 技術的詳細

この最適化の核心は、インターフェースのデータワードとitabワードへのアクセス方法の変更にあります。

以前の実装では、インターフェース変数`i`が与えられた場合、その内部構造(`i.data`と`i.tab`)にアクセスするために、まず`i`のアドレスをレジスタにロードし、その後`LEA`命令を使って`i.data`や`i.tab`のアドレスを計算し、さらにそのアドレスから値をロードするという手順を踏んでいたと考えられます。

新しいアプローチでは、インターフェース変数`i`がメモリ上に存在する場合、そのメモリ上の位置から直接`i.data`と`i.tab`の値をロードします。これにより、`LEA`命令によるアドレス計算のステップが不要になります。

具体的には、インターフェース値`i`がレジスタにロードされた後、そのレジスタをベースとして、適切なオフセット(`widthptr`はポインタのサイズ、32ビットシステムでは4バイト、64ビットシステムでは8バイト)を加算することで、`i.data`と`i.tab`のメモリ上の位置を直接参照し、`MOV`命令などで値をロードします。

この変更により、以下の利点が得られます。

*   **命令数の削減**: `LEA`命令が不要になるため、生成されるアセンブリコードの命令数が減ります。
*   **一時レジスタの削減**: `LEA`命令の結果を保持するための一時的なレジスタが不要になる場合があります。
*   **パフォーマンス向上**: 命令数の削減とレジスタ使用の効率化により、CPUの実行効率が向上し、インターフェースメソッド呼び出しのオーバーヘッドが減少します。

これは、コンパイラのバックエンドにおける典型的な最適化の一例であり、特定の命令の特性とデータ構造のレイアウトを理解することで、より効率的なコードを生成するものです。

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

変更は主に`src/cmd/6g/ggen.c`と`src/cmd/8g/ggen.c`の`cgen_callinter`関数に集中しています。この関数は、インターフェースを介したメソッド呼び出しのコード生成を担当しています。

### `src/cmd/6g/ggen.c` の変更点

```diff
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -105,7 +105,7 @@ void
 cgen_callinter(Node *n, Node *res, int proc)
 {
 	Node *i, *f;
-	Node tmpi, nodo, nodr, nodsp;
+	Node tmpi, nodi, nodo, nodr, nodsp; // nodi が追加
 
 	i = n->left;
 	if(i->op != ODOTINTER)
@@ -125,19 +125,25 @@ cgen_callinter(Node *n, Node *res, int proc)
 
 	genlist(n->list);		// assign the args
 
-	regalloc(&nodr, types[tptr], res);
-	regalloc(&nodo, types[tptr], &nodr);
-	nodo.op = OINDREG;
-
-	agen(i, &nodr);         // REG = &inter (インターフェースのアドレスをレジスタにロード)
+	// i is now addable, prepare an indirected
+	// register to hold its address.
+	igen(i, &nodi, res);		// REG = &inter (インターフェースのアドレスをレジスタにロード)
 
 	nodindreg(&nodsp, types[tptr], D_SP);
-	nodo.xoffset += widthptr;
-	cgen(&nodo, &nodsp);	// 0(SP) = 8(REG) -- i.data (データワードをスタックにコピー)
-
-	nodo.xoffset -= widthptr;
-	cgen(&nodo, &nodr);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
-
+	nodi.type = types[tptr];
+	nodi.xoffset += widthptr;
+	cgen(&nodi, &nodsp);	// 0(SP) = 8(REG) -- i.data (データワードをスタックにコピー)
+
+	regalloc(&nodo, types[tptr], res);
+	nodi.type = types[tptr];
+	nodi.xoffset -= widthptr;
+	cgen(&nodi, &nodo);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
+	regfree(&nodi); // nodi を解放
+
+	regalloc(&nodr, types[tptr], &nodo);
+	if(n->left->xoffset == BADWIDTH)
+		fatal("cgen_callinter: badwidth");
+	nodo.op = OINDREG; // OINDREG をここで設定
 	nodo.xoffset = n->left->xoffset + 3*widthptr + 8;
 	cgen(&nodo, &nodr);	// REG = 32+offset(REG) -- i.tab->fun[f]
 
@@ -185,7 +191,7 @@ cgen_call(Node *n, int proc)
 		tnod.type = t;
 		ginscall(&nod, proc);
 		regfree(&nod);
-		goto ret; // goto ret を return に変更
+		return;
 	}
 
 	// call pointer
@@ -195,16 +201,12 @@ cgen_call(Node *n, int proc)
 		tnod.type = t;
 		ginscall(&nod, proc);
 		regfree(&nod);
-		goto ret; // goto ret を return に変更
+		return;
 	}
 
 	// call direct
 	n->left->method = 1;
 	ginscall(n->left, proc);
-
-
-ret: // ret ラベルを削除
-	;
 }
 
 /*

src/cmd/8g/ggen.c の変更点

src/cmd/8g/ggen.cも同様の変更が加えられています。主な違いは、32ビットアーキテクチャであるため、widthptrの値が異なる点と、オフセット計算が若干異なる点です。

--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -146,7 +146,7 @@ void
 cgen_callinter(Node *n, Node *res, int proc)
 {
 	Node *i, *f;
-	Node tmpi, nodo, nodr, nodsp;
+	Node tmpi, nodi, nodo, nodr, nodsp; // nodi が追加
 
 	i = n->left;
 	if(i->op != ODOTINTER)
@@ -166,23 +166,25 @@ cgen_callinter(Node *n, Node *res, int proc)
 
 	genlist(n->list);		// assign the args
 
-	// Can regalloc now; i is known to be addable,
-	// so the agen will be easy.
-	regalloc(&nodr, types[tptr], res);
-	regalloc(&nodo, types[tptr], &nodr);
-	nodo.op = OINDREG;
-
-	agen(i, &nodr);		// REG = &inter (インターフェースのアドレスをレジスタにロード)
+	// i is now addable, prepare an indirected
+	// register to hold its address.
+	igen(i, &nodi, res);		// REG = &inter (インターフェースのアドレスをレジスタにロード)
 
 	nodindreg(&nodsp, types[tptr], D_SP);
-	nodo.xoffset += widthptr;
-	cgen(&nodo, &nodsp);	// 0(SP) = 4(REG) -- i.data (データワードをスタックにコピー)
+	nodi.type = types[tptr];
+	nodi.xoffset += widthptr;
+	cgen(&nodi, &nodsp);	// 0(SP) = 4(REG) -- i.data (データワードをスタックにコピー)
 
-	nodo.xoffset -= widthptr;
-	cgen(&nodo, &nodr);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
+	regalloc(&nodo, types[tptr], res);
+	nodi.type = types[tptr];
+	nodi.xoffset -= widthptr;
+	cgen(&nodi, &nodo);	// REG = 0(REG) -- i.tab (itabワードをレジスタにロード)
+	regfree(&nodi); // nodi を解放
 
+\tregalloc(&nodr, types[tptr], &nodo);
 	if(n->left->xoffset == BADWIDTH)
 		fatal("cgen_callinter: badwidth");
+\tnodo.op = OINDREG; // OINDREG をここで設定
 	nodo.xoffset = n->left->xoffset + 3*widthptr + 8;
 	cgen(&nodo, &nodr);	// REG = 20+offset(REG) -- i.tab->fun[f]
 

コアとなるコードの解説

cgen_callinter関数は、インターフェースn->leftを介してメソッドを呼び出すためのコードを生成します。

  1. Node tmpi, nodi, nodo, nodr, nodsp;:

    • nodiという新しいNode変数が追加されています。これは、インターフェースのアドレスを保持するための一時的なノードとして使用されます。以前はnodoがこの役割の一部を担っていましたが、より明確な役割分担がなされています。
  2. igen(i, &nodi, res);:

    • igen関数は、ノードi(インターフェース変数)のアドレスを計算し、その結果をnodiに格納します。これは、インターフェース値がメモリ上のどこにあるかを特定する最初のステップです。以前のagen(i, &nodr)と異なり、igenは間接参照を考慮したコード生成を行います。
  3. データワードのロード (i.data):

    • nodi.xoffset += widthptr;nodiが指すアドレスにwidthptr(ポインタのサイズ)を加算することで、インターフェースのデータワードのメモリ上の位置を指すようにします。Goのインターフェースは通常、itabワードの次にデータワードが配置されるため、itabワードのサイズ分オフセットを移動します。
    • cgen(&nodi, &nodsp);nodiが指すメモリ位置からデータワードをロードし、それをスタックポインタnodspが指す位置(通常はスタックの先頭)にコピーします。これは、メソッド呼び出しの引数としてデータワードを渡す準備です。
  4. itabワードのロード (i.tab):

    • regalloc(&nodo, types[tptr], res);nodoという新しいレジスタを割り当てます。このレジスタはitabワードを保持するために使用されます。
    • nodi.xoffset -= widthptr;nodiのオフセットを元に戻し、インターフェースの先頭(itabワードの開始位置)を指すようにします。
    • cgen(&nodi, &nodo);nodiが指すメモリ位置からitabワードをロードし、それをnodoレジスタに格納します。
    • regfree(&nodi);nodiはもう必要ないので、関連するレジスタを解放します。
  5. メソッドポインタのルックアップ:

    • regalloc(&nodr, types[tptr], &nodo);nodrというレジスタを割り当て、nodo(itabワードを保持)をベースとします。
    • nodo.op = OINDREG;nodoを間接参照ノードとして設定します。
    • nodo.xoffset = n->left->xoffset + 3*widthptr + 8;nodoが指すitabテーブル内の、呼び出すべきメソッドのポインタへのオフセットを計算します。このオフセットは、itabの構造(型情報、メソッド数、そしてメソッドポインタの配列)に依存します。
    • cgen(&nodo, &nodr);nodoが指すメモリ位置(itab内のメソッドポインタ)から、実際のメソッドの開始アドレスをロードし、それをnodrレジスタに格納します。

この一連の変更により、インターフェースのデータとitabへのアクセスが、LEA命令を介した間接的なアドレス計算ではなく、直接的なメモリロードに置き換えられています。これにより、生成されるアセンブリコードがより効率的になります。

また、cgen_call関数内のgoto ret;return;に置き換えられ、不要なret:ラベルが削除されています。これはコードの可読性と保守性を向上させるためのクリーンアップです。

関連リンク

参考にした情報源リンク