[インデックス 13256] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)において、スライス、配列、文字列のスライス操作(slice[arr,str]
)の処理を、主にコンパイラのフロントエンドでインライン化する変更を導入しています。これにより、これらの操作がランタイム関数呼び出しとして処理されるのではなく、コンパイル時に直接コードが生成されるようになります。結果として、ランタイムの複雑性が軽減され、生成されるバイナリのパフォーマンスが向上する可能性があります。
コミット
commit 40af78c19eeceb38407c2b7c2a4d8b685249701f
Author: Luuk van Dijk <lvd@golang.org>
Date: Sat Jun 2 22:50:57 2012 -0400
cmd/gc: inline slice[arr,str] in the frontend (mostly).
R=rsc, ality, rogpeppe, minux.ma, dave
CC=golang-dev
https://golang.org/cl/5966075
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/40af78c19eeceb38407c2b7c2a4d8b685249701f
元コミット内容
cmd/gc: inline slice[arr,str] in the frontend (mostly).
R=rsc, ality, rogpeppe, minux.ma, dave
CC=golang-dev
https://golang.org/cl/5966075
変更の背景
Go言語において、スライス、配列、文字列のスライス操作は頻繁に行われる基本的な操作です。これらの操作がランタイム関数として実装されている場合、関数呼び出しのオーバーヘッドが発生し、パフォーマンスに影響を与える可能性があります。また、ランタイムに複雑なロジックが存在すると、コンパイラが最適化を行う機会が失われることもあります。
このコミットの背景には、以下の目的があったと考えられます。
- パフォーマンスの向上: スライス操作をコンパイラのフロントエンドでインライン化することで、ランタイム関数呼び出しのオーバーヘッドを排除し、より効率的な機械語コードを生成することを目指しています。これにより、スライス操作が多用されるコードの実行速度が向上します。
- コンパイラの最適化機会の増加: スライス操作のロジックがコンパイラ内部に組み込まれることで、コンパイラはより広範なコンテキストで最適化を適用できるようになります。例えば、定数によるスライス操作の場合、コンパイル時に結果を計算し、実行時の計算を省略するといった最適化が可能になります。
- ランタイムの簡素化: スライス操作に関するロジックをランタイムからコンパイラへ移行することで、ランタイムのコードベースが簡素化され、保守性が向上します。
Goコンパイラは、フロントエンド(構文解析、型チェック、AST変換など)とバックエンド(コード生成、最適化など)に分かれています。この変更は「フロントエンドでのインライン化」と明記されており、AST変換の段階でスライス操作をより低レベルな操作に分解し、バックエンドが直接処理できる形に変換することを示唆しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。
- Go言語のスライス、配列、文字列:
- 配列 (Array): 固定長で、要素の型が同じ値のシーケンスです。
[N]T
のように宣言され、N
は配列の長さ(要素数)を表します。 - スライス (Slice): 配列をラップした動的なビューです。長さ(
len
)と容量(cap
)を持ち、基底配列の一部を参照します。[]T
のように宣言されます。スライス操作(s[low:high]
など)は、既存のスライスや配列から新しいスライスを作成します。 - 文字列 (String): 読み取り専用のバイトスライスです。Goの文字列はUTF-8エンコードされており、内部的にはバイトのシーケンスとして扱われます。文字列のスライス操作も、バイトスライスと同様に機能します。
- 配列 (Array): 固定長で、要素の型が同じ値のシーケンスです。
- Goコンパイラの構造:
cmd/gc
: Go言語の公式コンパイラです。Goソースコードを機械語コードに変換します。- フロントエンド: ソースコードの字句解析、構文解析、抽象構文木(AST)の構築、型チェック、ASTの変換(
walk
フェーズなど)を担当します。この段階で、高レベルなGoの構文がコンパイラ内部のより低レベルな表現に変換されます。 - バックエンド: ASTから中間表現(IR)を生成し、最適化を行い、最終的にターゲットアーキテクチャ(例:
amd64
,arm
,386
)の機械語コードを生成します。cmd/5g
,cmd/6g
,cmd/8g
はそれぞれarm
,amd64
,386
アーキテクチャ向けのバックエンドコンパイラです。 src/pkg/runtime
: Goプログラムの実行をサポートするランタイムライブラリです。ガベージコレクション、スケジューラ、プリミティブなデータ構造の操作などが含まれます。以前は、スライス操作の一部もここにランタイム関数として実装されていました。
- インライン化 (Inlining): コンパイラ最適化の一種で、関数呼び出しをその関数の本体のコードで直接置き換えることです。これにより、関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、引数の渡し、戻り値の処理など)が削減され、パフォーマンスが向上します。また、インライン化されたコードに対して、さらに多くの最適化を適用できるようになります。
Node
: Goコンパイラ内部でAST(抽象構文木)のノードを表すデータ構造です。Goのプログラムの各要素(変数、式、文など)はNode
として表現されます。OXXX
: Goコンパイラ内部でASTノードの操作を表すオペレーションコードです。例えば、OSLICE
はスライス操作、OSLICEARR
は配列のスライス操作、OSLICESTR
は文字列のスライス操作を表します。cgen
: "code generation"の略で、ASTノードから機械語コードを生成する関数群を指します。walk
: コンパイラのフロントエンドにおけるAST変換フェーズの一つです。このフェーズで、高レベルなGoの構文がより低レベルな中間表現に変換されます。
技術的詳細
このコミットの主要な技術的変更は、Goのスライス、配列、文字列のスライス操作(s[low:high]
など)の処理を、ランタイム関数呼び出しからコンパイラのフロントエンドでの直接的なコード生成へと移行した点にあります。
以前のGoコンパイラでは、これらのスライス操作はruntime
パッケージ内のヘルパー関数(例: runtime.sliceslice
, runtime.sliceslice1
, runtime.slicearray
, runtime.slicestring
)を呼び出すように変換されていました。これらの関数は、スライスの長さ、容量、基底配列のポインタを計算し、境界チェックを行う役割を担っていました。
このコミットでは、以下の変更が行われています。
runtime
パッケージからのスライス関連関数の削除:src/pkg/runtime/slice.c
からruntime.sliceslice
およびruntime.sliceslice1
関数が削除されました。src/cmd/gc/builtin.c
およびsrc/cmd/gc/runtime.go
から、これらのランタイム関数の宣言が削除されました。src/pkg/runtime/string.goc
から文字列スライス関連の関数が削除されました。
- コンパイラフロントエンド(
cmd/gc/walk.c
)での処理のインライン化:walk.c
内のwalkexpr
関数において、OSLICE
,OSLICEARR
,OSLICESTR
オペレーションの処理が大幅に変更されました。- 新しい静的関数
sliceany
が導入され、スライス操作の境界チェックと、新しいスライスの長さ、容量、基底配列のオフセットの計算が、コンパイル時に直接ASTノードとして表現されるようになりました。 sliceany
関数は、スライス操作の引数(low
,high
インデックス)を評価し、静的な境界チェック(コンパイル時に値が確定している場合)と動的な境界チェック(実行時に値が確定する場合)の両方を処理します。動的な境界チェックは、panicslice()
ランタイム関数を呼び出す条件分岐としてASTに挿入されます。sliceany
は、最終的にスライス操作の結果を表すNode
のlist
フィールドに、新しいスライスの容量、長さ、および基底配列へのオフセットを表すノードを設定します。
- コンパイラバックエンド(
cmd/gc/gen.c
およびcmd/xg/cgen.c
,cmd/xg/ggen.c
,cmd/xg/gsubr.c
)でのコード生成の変更:src/cmd/gc/gen.c
に新しい関数cgen_slice
が追加されました。この関数は、walk.c
で変換されたスライス操作のASTノードを受け取り、実際の機械語コードを生成します。cgen_slice
は、新しいスライスの長さ、容量、および基底配列のポインタを計算するための命令を生成します。これには、元のスライスの情報と、sliceany
で計算されたオフセットが使用されます。- 各アーキテクチャ固有のバックエンド(
cmd/5g
,cmd/6g
,cmd/8g
)のcgen.c
ファイルでは、OSLICE
,OSLICEARR
,OSLICESTR
オペレーションが直接cgen_slice
を呼び出すように変更されました。これにより、以前のcgen_inline
関数(ランタイム関数呼び出しをインライン化していたもの)が不要になり、削除されました。 gsubr.c
にcheckref
関数が追加され、nilポインタ参照時にセグメンテーション違反を強制するための命令を生成するようになりました。これは、スライス操作がランタイムからコンパイラに移行したことで、nilポインタチェックのメカニズムを調整する必要があったためと考えられます。
この変更により、スライス操作はコンパイル時に直接機械語命令に展開されるため、実行時の関数呼び出しオーバーヘッドがなくなります。また、コンパイラはスライス操作のコンテキストをより深く理解できるため、定数伝播などの最適化を適用しやすくなります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと関数は以下の通りです。
src/cmd/gc/walk.c
:walkexpr
関数内のOSLICE
,OSLICEARR
,OSLICESTR
の処理ロジックが大幅に書き換えられました。- 新しい静的関数
sliceany
が追加され、スライス操作のフロントエンド処理(境界チェック、長さ・容量・オフセットの計算)を担当します。
src/cmd/gc/gen.c
:- 新しい関数
cgen_slice
が追加され、walk.c
で変換されたスライス操作のASTノードから機械語コードを生成します。
- 新しい関数
src/cmd/5g/cgen.c
,src/cmd/6g/cgen.c
,src/cmd/8g/cgen.c
:- 各アーキテクチャの
cgen
関数内で、OSLICE
,OSLICEARR
,OSLICESTR
オペレーションが直接cgen_slice
を呼び出すように変更されました。 - 以前存在した
cgen_inline
関数が削除されました。
- 各アーキテクチャの
src/cmd/5g/ggen.c
,src/cmd/6g/ggen.c
,src/cmd/8g/ggen.c
:cgen_inline
関数および関連するヘルパー関数(regcmp
,fix64
,getargs
,cmpandthrow
,sleasy
)が削除されました。これらの関数は、以前ランタイム関数呼び出しをインライン化するために使用されていました。
src/cmd/5g/gsubr.c
,src/cmd/6g/gsubr.c
,src/cmd/8g/gsubr.c
:checkref
関数が追加され、nilポインタ参照時のセグメンテーション違反を強制する命令を生成します。
src/cmd/gc/builtin.c
,src/cmd/gc/runtime.go
:sliceslice
,sliceslice1
,slicearray
といったランタイム関数の宣言が削除されました。
src/pkg/runtime/slice.c
,src/pkg/runtime/string.goc
:- スライスおよび文字列のスライス操作に関するランタイム関数(
runtime.sliceslice
,runtime.sliceslice1
など)の実装が削除されました。
- スライスおよび文字列のスライス操作に関するランタイム関数(
コアとなるコードの解説
src/cmd/gc/walk.c
の変更点 (sliceany
関数)
sliceany
関数は、スライス操作のフロントエンド処理の核心です。
static Node*
sliceany(Node* n, NodeList **init)
{
// ... (変数の宣言と初期化)
src = n->left; // 元のスライス/配列/文字列
lb = n->right->left; // low インデックス
hb = n->right->right; // high インデックス
// ... (境界値の計算: bound は元のスライス/配列/文字列の長さまたは容量)
// 静的な境界チェック (コンパイル時に値が確定している場合)
// 例: s[0:10] で s の長さが 5 の場合、コンパイルエラーにする
if(isconst(hb, CTINT)) {
hbv = mpgetfix(hb->val.u.xval);
if(hbv < 0 || hbv > bv || !smallintconst(hb)) {
yyerror("slice index out of bounds");
hbv = -1;
}
}
// ... (lbv の静的チェックも同様)
if(lbv >= 0 && hbv >= 0 && lbv > hbv)
yyerror("inverted slice range"); // 例: s[5:0]
// 動的な境界チェック (実行時に値が確定する場合)
// 例: if hb > bound || lb > hb { panicslice() }
chk = N;
chk1 = N;
chk2 = N;
// ... (hb, lb を適切な型に変換)
if(hb != N) {
hb = cheapexpr(conv(hb, bt), init);
if(!bounded) // bounded フラグがない場合(つまり、コンパイル時に境界チェックが省略できない場合)
chk1 = nod(OLT, bound, hb); // bound < hb の場合、エラー
}
// ... (lb の動的チェックも同様)
if(chk1 != N || chk2 != N) {
chk = nod(OIF, N, N);
chk->nbody = list1(mkcall("panicslice", T, init)); // 境界外の場合、panicslice を呼び出す
if(chk1 != N)
chk->ntest = chk1;
if(chk2 != N) {
if(chk->ntest == N)
chk->ntest = chk2;
else
chk->ntest = nod(OOROR, chk->ntest, chk2); // 論理 OR で結合
}
typecheck(&chk, Etop);
walkstmt(&chk);
*init = concat(*init, chk->ninit);
chk->ninit = nil;
*init = list(*init, chk); // ASTに境界チェックのif文を追加
}
// バックエンドの cgen_slice のために、新しい cap, len, offs を準備
// cap = bound [ - lo ]
n->right = N; // 元の right ノードは不要になる
n->list = nil;
if(lb == N)
bound = conv(bound, types[TUINT32]);
else
bound = nod(OSUB, conv(bound, types[TUINT32]), conv(lb, types[TUINT32]));
typecheck(&bound, Erv);
walkexpr(&bound, init);
n->list = list(n->list, bound); // 新しい容量
// len = hi [ - lo]
if(lb == N)
hb = conv(hb, types[TUINT32]);
else
hb = nod(OSUB, conv(hb, types[TUINT32]), conv(lb, types[TUINT32]));
typecheck(&hb, Erv);
walkexpr(&hb, init);
n->list = list(n->list, hb); // 新しい長さ
// offs = [width *] lo, but omit if zero
if(lb != N) {
// ... (要素の幅 w を計算)
lb = conv(lb, types[TUINTPTR]);
if(w > 1)
lb = nod(OMUL, nodintconst(w), lb); // オフセット = low * 要素の幅
typecheck(&lb, Erv);
walkexpr(&lb, init);
n->list = list(n->list, lb); // 基底配列へのオフセット
}
return n;
}
sliceany
関数は、スライス操作のASTノードを受け取り、以下の処理を行います。
- 引数の抽出: 元のスライス/配列/文字列 (
src
)、low
インデックス (lb
)、high
インデックス (hb
) を抽出します。 - 境界値の計算: 元のデータ構造の長さまたは容量 (
bound
) を計算します。 - 静的境界チェック:
low
やhigh
が定数である場合、コンパイル時に境界外アクセスや範囲の逆転がないかチェックし、エラーを報告します。 - 動的境界チェック:
low
やhigh
が実行時に決定される場合、if
文とpanicslice()
関数呼び出しを含むASTノードを生成し、init
リストに追加します。これにより、実行時に境界チェックが行われます。 - 結果の準備: 新しいスライスの長さ、容量、および基底配列へのオフセットを計算し、これらをASTノードとして元のスライス操作ノードの
list
フィールドに格納します。この情報は、バックエンドのcgen_slice
関数が機械語コードを生成する際に使用されます。
src/cmd/gc/gen.c
の変更点 (cgen_slice
関数)
cgen_slice
関数は、sliceany
によって準備された情報に基づいて、実際の機械語コードを生成します。
void
cgen_slice(Node *n, Node *res)
{
Node src, dst, *cap, *len, *offs, *add;
// n->list は sliceany で設定された (cap, len, offs) のリスト
cap = n->list->n; // 新しいスライスの容量
len = n->list->next->n; // 新しいスライスの長さ
offs = N;
if(n->list->next->next)
offs = n->list->next->next->n; // 基底配列へのオフセット
// dst.len = hi [ - lo ]
dst = *res;
dst.xoffset += Array_nel; // スライス構造体の len フィールドへのオフセット
dst.type = types[TUINT32];
cgen(len, &dst); // 新しい長さを dst.len に代入するコードを生成
if(n->op != OSLICESTR) { // 文字列スライス以外の場合
// dst.cap = cap [ - lo ]
dst = *res;
dst.xoffset += Array_cap; // スライス構造体の cap フィールドへのオフセット
dst.type = types[TUINT32];
cgen(cap, &dst); // 新しい容量を dst.cap に代入するコードを生成
}
// dst.array = src.array [ + lo *width ]
dst = *res;
dst.xoffset += Array_array; // スライス構造体の array フィールドへのオフセット
dst.type = types[TUINTPTR];
if(n->op == OSLICEARR) {
// 配列スライスの場合、nilポインタチェックを強制
checkref(n->left);
}
src = *n->left;
src.xoffset += Array_array; // 元のスライス/配列の array フィールドへのオフセット
src.type = types[TUINTPTR];
if(offs == N) { // オフセットがない場合 (low が 0 の場合など)
cgen(&src, &dst); // 元の array をそのまま dst.array に代入
} else {
add = nod(OADD, &src, offs); // 元の array + オフセット
typecheck(&add, Erv);
cgen(add, &dst); // 計算結果を dst.array に代入
}
}
cgen_slice
関数は、sliceany
で計算された長さ、容量、オフセットのノードを受け取り、それらをターゲットスライス構造体の対応するフィールドに格納するための機械語命令を生成します。特に、基底配列のポインタは、元のスライスの基底配列ポインタに計算されたオフセットを加算することで求められます。
src/pkg/runtime/slice.c
および src/cmd/xg/ggen.c
からの削除
runtime.sliceslice
や runtime.sliceslice1
といった関数は、Goのランタイムライブラリから完全に削除されました。これは、これらの操作がもはやランタイムの責任ではなく、コンパイラが直接処理するようになったことを意味します。
また、各アーキテクチャの ggen.c
ファイルから cgen_inline
関数が削除されました。この関数は、以前はランタイム関数呼び出しをインライン化するために使用されていましたが、スライス操作のロジックがコンパイラのフロントエンドに完全に移行したため、不要になりました。
関連リンク
- Go Change-list: https://golang.org/cl/5966075
参考にした情報源リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Goコンパイラのソースコード (GitHub): https://github.com/golang/go
- Go Slices: usage and internals: https://go.dev/blog/slices-intro
- Go Data Structures: Slices: https://go.dev/blog/go-slices-usage-and-internals
- A Quick Guide to Go's Assembler: https://go.dev/doc/asm
- Go Compiler Internals (非公式): https://go.dev/talks/2015/go-compiler-internals.slide (これは古い情報ですが、基本的な概念の理解に役立ちます)
- Compiler Explorer (Godbolt): https://godbolt.org/ (Goコードがどのようにアセンブリにコンパイルされるかを視覚的に確認するのに役立ちます)