[インデックス 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つのポインタ(またはワード)で構成されます。
- データワード (Data Word): インターフェース値が保持する具体的な値(またはその値へのポインタ)を指します。これは、インターフェースに格納されている実際のデータです。
- 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
の変更点
--- 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
を介してメソッドを呼び出すためのコードを生成します。
-
Node tmpi, nodi, nodo, nodr, nodsp;
:nodi
という新しいNode
変数が追加されています。これは、インターフェースのアドレスを保持するための一時的なノードとして使用されます。以前はnodo
がこの役割の一部を担っていましたが、より明確な役割分担がなされています。
-
igen(i, &nodi, res);
:igen
関数は、ノードi
(インターフェース変数)のアドレスを計算し、その結果をnodi
に格納します。これは、インターフェース値がメモリ上のどこにあるかを特定する最初のステップです。以前のagen(i, &nodr)
と異なり、igen
は間接参照を考慮したコード生成を行います。
-
データワードのロード (
i.data
):nodi.xoffset += widthptr;
:nodi
が指すアドレスにwidthptr
(ポインタのサイズ)を加算することで、インターフェースのデータワードのメモリ上の位置を指すようにします。Goのインターフェースは通常、itabワードの次にデータワードが配置されるため、itabワードのサイズ分オフセットを移動します。cgen(&nodi, &nodsp);
:nodi
が指すメモリ位置からデータワードをロードし、それをスタックポインタnodsp
が指す位置(通常はスタックの先頭)にコピーします。これは、メソッド呼び出しの引数としてデータワードを渡す準備です。
-
itabワードのロード (
i.tab
):regalloc(&nodo, types[tptr], res);
:nodo
という新しいレジスタを割り当てます。このレジスタはitabワードを保持するために使用されます。nodi.xoffset -= widthptr;
:nodi
のオフセットを元に戻し、インターフェースの先頭(itabワードの開始位置)を指すようにします。cgen(&nodi, &nodo);
:nodi
が指すメモリ位置からitabワードをロードし、それをnodo
レジスタに格納します。regfree(&nodi);
:nodi
はもう必要ないので、関連するレジスタを解放します。
-
メソッドポインタのルックアップ:
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言語のインターフェースに関する公式ドキュメント: https://go.dev/doc/effective_go#interfaces
- Goコンパイラのソースコード: https://github.com/golang/go
- このコミットが解決したIssue: https://go.dev/issue/1914 (Go issue tracker)
- このコミットのGo Gerrit Code Reviewページ: https://golang.org/cl/6501110
参考にした情報源リンク
- Go言語のインターフェースの内部表現に関する記事やドキュメント (例: "Go Data Structures: Interfaces" by Russ Cox, "The Laws of Reflection" by Rob Pikeなど)
- x86/AMD64アーキテクチャの
LEA
命令に関するアセンブリ言語のドキュメント - Goコンパイラのソースコード(特に
src/cmd/6g/ggen.c
とsrc/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
を介してメソッドを呼び出すためのコードを生成します。
-
Node tmpi, nodi, nodo, nodr, nodsp;
:nodi
という新しいNode
変数が追加されています。これは、インターフェースのアドレスを保持するための一時的なノードとして使用されます。以前はnodo
がこの役割の一部を担っていましたが、より明確な役割分担がなされています。
-
igen(i, &nodi, res);
:igen
関数は、ノードi
(インターフェース変数)のアドレスを計算し、その結果をnodi
に格納します。これは、インターフェース値がメモリ上のどこにあるかを特定する最初のステップです。以前のagen(i, &nodr)
と異なり、igen
は間接参照を考慮したコード生成を行います。
-
データワードのロード (
i.data
):nodi.xoffset += widthptr;
:nodi
が指すアドレスにwidthptr
(ポインタのサイズ)を加算することで、インターフェースのデータワードのメモリ上の位置を指すようにします。Goのインターフェースは通常、itabワードの次にデータワードが配置されるため、itabワードのサイズ分オフセットを移動します。cgen(&nodi, &nodsp);
:nodi
が指すメモリ位置からデータワードをロードし、それをスタックポインタnodsp
が指す位置(通常はスタックの先頭)にコピーします。これは、メソッド呼び出しの引数としてデータワードを渡す準備です。
-
itabワードのロード (
i.tab
):regalloc(&nodo, types[tptr], res);
:nodo
という新しいレジスタを割り当てます。このレジスタはitabワードを保持するために使用されます。nodi.xoffset -= widthptr;
:nodi
のオフセットを元に戻し、インターフェースの先頭(itabワードの開始位置)を指すようにします。cgen(&nodi, &nodo);
:nodi
が指すメモリ位置からitabワードをロードし、それをnodo
レジスタに格納します。regfree(&nodi);
:nodi
はもう必要ないので、関連するレジスタを解放します。
-
メソッドポインタのルックアップ:
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言語のインターフェースに関する公式ドキュメント: https://go.dev/doc/effective_go#interfaces
- Goコンパイラのソースコード: https://github.com/golang/go
- このコミットが解決したIssue: https://go.dev/issue/1914 (Go issue tracker)
- このコミットのGo Gerrit Code Reviewページ: https://golang.org/cl/6501110
参考にした情報源リンク
- Go言語のインターフェースの内部表現に関する記事やドキュメント (例: "Go Data Structures: Interfaces" by Russ Cox, "The Laws of Reflection" by Rob Pikeなど)
- x86/AMD64アーキテクチャの
LEA
命令に関するアセンブリ言語のドキュメント - Goコンパイラのソースコード(特に
src/cmd/6g/ggen.c
とsrc/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/go