[インデックス 1367] ファイルの概要
このコミットは、Go言語のコンパイラ (gc, 6g) とランタイムにおける配列(特にスライス)の内部表現と操作に関する根本的な変更を導入しています。Go言語の初期段階において、配列とスライスの概念がどのように進化し、現在の形に近づいていったかを示す重要なコミットの一つです。
変更されたファイルは以下の通りです。
src/cmd/6g/align.c: 型のアライメントとサイズの計算に関する変更。特に配列のサイズ計算ロジックが更新されています。src/cmd/6g/cgen.c: Goコンパイラの6g(x86-64アーキテクチャ向け)におけるコード生成部分。配列の長さ (LEN)、容量 (CAP)、スライス操作、および配列間の変換に関するコード生成ロジックが修正されています。src/cmd/gc/dcl.c: Goコンパイラの宣言処理部分。構造体フィールドにおける「オープン配列」(サイズが未定の配列)の制限が緩和されています。src/cmd/gc/go.h: Goコンパイラの共通ヘッダファイル。Array構造体の定義が変更され、新しい型チェックヘルパー関数(issarray,isdarrayなど)が追加されています。src/cmd/gc/go.y: Go言語の文法定義ファイル(Yacc/Bison)。newキーワードの動作が、ポインタではなく直接型を返すように変更されています。src/cmd/gc/subr.c: Goコンパイラのサブルーチン集。新しい型チェックヘルパー関数の実装と、配列のインデックス処理に関する変更が含まれています。src/cmd/gc/sys.go: Go言語の組み込みシステム関数定義。配列を引数や戻り値とする関数のシグネチャが、ポインタ型 (*[]any) から値型 ([]any) へと変更されています。src/cmd/gc/sysimport.c: システム関数のインポート宣言。sys.goの変更に合わせて更新されています。src/cmd/gc/walk.c: GoコンパイラのASTウォーク(構文木走査)処理。new式やスライス操作の型チェックとコード変換ロジックが大幅に修正されています。src/runtime/array.c: Goランタイムにおける配列操作の低レベル実装。newarray,arraysliced,arrayslices,arrays2dといった関数が、配列(スライス)のヘッダを値として受け渡し、直接操作するように変更されています。src/runtime/runtime.h: Goランタイムの共通ヘッダファイル。Array構造体の定義が変更され、内部のbフィールドが削除されています。test/ken/array.go,test/ken/chan.go,test/ken/range.go: テストファイル。配列の受け渡し方法の変更(ポインタから値へ)に合わせてテストコードが修正されています。
コミット
commit 4026500d1873953ef76b9a21122cd7b934c23503
Author: Ken Thompson <ken@golang.org>
Date: Thu Dec 18 20:06:28 2008 -0800
arrays
R=r
OCL=21564
CL=21564
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4026500d1873953ef76b9a21122cd7b934c23503
元コミット内容
このコミットは「arrays」という簡潔なメッセージで、Go言語における配列(特にスライス)の内部実装とセマンティクスに関する変更を示唆しています。具体的には、配列の表現方法、コンパイラによる処理、およびランタイムでの操作方法が更新されています。
変更の背景
Go言語の初期設計段階では、配列とスライスの概念がまだ完全に固まっていませんでした。このコミットが行われた2008年12月時点では、Goのスライスは「動的配列 (dynamic array)」と呼ばれ、その内部表現やコンパイラ・ランタイムでの扱いが試行錯誤されていました。
このコミットの主な背景は、Goのスライスが持つべきセマンティクス、すなわち「値渡しされるが、内部的には共有された基底配列を参照する」という特性を、より効率的かつ整合性のある形で実装することにありました。以前の実装では、スライス(動的配列)がポインタを介して扱われることが多く、これはC言語の配列ポインタに近いものでした。しかし、Goのスライスは、長さ、容量、そして基底配列へのポインタという3つの要素を持つ「デスクリプタ」(または「ドープベクタ」)として振る舞うべきであり、このデスクリプタ自体は値としてコピーされるべきです。
このコミットは、以下の問題点を解決しようとしています。
- 一貫性のない配列/スライス表現: コンパイラとランタイムの間で、配列やスライスの型情報やメモリレイアウトに関する認識に不整合があった可能性があります。
- 非効率なポインタ渡し: スライスをポインタで渡すことは、Goのセマンティクス(値渡し)に反し、コードの複雑性を増す可能性がありました。スライスヘッダ自体は小さいため、値渡しの方が効率的で自然です。
newキーワードのセマンティクス:newキーワードが配列に対して使用された際の挙動が、スライスのセマンティクスと合致していなかった可能性があります。- ランタイム関数の整合性:
newarrayやarrayslicedといったランタイム関数が、新しいスライスのセマンティクスに合わせて更新される必要がありました。
これらの変更は、Go言語が現在持つ、強力で使いやすいスライス機能の基盤を築く上で不可欠なステップでした。
前提知識の解説
このコミットを理解するためには、Go言語の初期の配列とスライスの概念、およびコンパイラとランタイムの基本的な役割について理解しておく必要があります。
Go言語の配列とスライス(初期の概念)
- 配列 (Array): Goにおける配列は、固定長で要素の型が同じシーケンスです。例えば
[10]intは10個の整数を格納できる配列です。Goの配列は値型であり、代入や関数への引数として渡される際には、配列全体のコピーが作成されます。 - スライス (Slice): Goのスライスは、配列の一部を参照する動的なビューです。スライスは、基底配列へのポインタ、長さ (length)、容量 (capacity) の3つの要素からなる「スライスヘッダ」(または「ドープベクタ」)で構成されます。スライスは値型であり、スライスヘッダ自体は値としてコピーされますが、そのヘッダが指す基底配列のデータは共有されます。このコミット以前は、「動的配列」という用語が使われていました。
コンパイラ (gc, 6g)
Go言語のコンパイラは、Goのソースコードを機械語に変換する役割を担います。このコミットで変更されている src/cmd/gc はGoの主要なコンパイラ(gc は汎用的なコンパイラフロントエンド、6g はx86-64アーキテクチャ向けのバックエンド)のソースコードです。
align.c: 型のメモリ配置(アライメント)とサイズを計算します。これは、構造体や配列のメモリ効率と正しいアクセスに不可欠です。cgen.c: コード生成フェーズの一部で、Goの抽象構文木 (AST) をターゲットアーキテクチャの機械語命令に変換します。dcl.c: 変数や型の宣言を処理します。go.h: コンパイラ全体で共有される型定義やマクロが含まれます。go.y: Go言語の文法を定義するYacc/Bisonファイル。これにより、Goのソースコードがどのように解析されるかが決まります。subr.c: コンパイラ内で使用される様々なユーティリティ関数やサブルーチンが含まれます。walk.c: コンパイラの「ウォーク」フェーズで、ASTを走査し、型チェック、最適化、コード変換などの処理を行います。
ランタイム (runtime)
Goランタイムは、Goプログラムの実行をサポートする低レベルのコードです。ガベージコレクション、スケジューリング、チャネル操作、そして配列やスライスの基本的な操作などが含まれます。
array.c: スライスの作成、スライス、コピーなどの低レベルな操作を実装するC言語のファイルです。runtime.h: ランタイム全体で共有される型定義やマクロが含まれます。
ドープベクタ (Dope Vector)
スライスは、内部的に「ドープベクタ」と呼ばれる構造体で表現されます。これは通常、以下の3つのフィールドを持ちます。
- ポインタ: スライスの要素が格納されている基底配列の先頭要素へのポインタ。
- 長さ (Length): スライスが現在含んでいる要素の数。
- 容量 (Capacity): 基底配列の先頭から、スライスが拡張できる最大の要素数。
このコミットは、このドープベクタの表現と、それが関数間でどのように受け渡されるかを変更しています。
技術的詳細
このコミットの技術的詳細は、主に以下の3つの側面に集約されます。
-
Array構造体の変更: コンパイラとランタイムの両方で定義されているArray構造体が、スライスヘッダの純粋な表現へと変更されました。src/cmd/gc/go.hでは、Array構造体のnel(number of elements) とcap(capacity) フィールドがuint32型としてではなく、uchar[4]のようにバイト配列として定義されています。これは、C言語の構造体パディングやアライメントを考慮し、メモリレイアウトを厳密に制御するための低レベルな表現です。array[8]は基底配列へのポインタを表します。src/runtime/runtime.hでは、Array構造体からbyte b[8];フィールドが削除されました。このbフィールドは、以前は小さな配列をArray構造体内に直接埋め込むためのものであったか、あるいは単なるプレースホルダーであった可能性があります。この削除により、Array構造体は純粋に基底配列へのポインタ、長さ、容量のみを持つドープベクタとして機能するようになります。これは、スライスが常に外部のメモリ領域を参照するというGoの現在のセマンティクスと一致します。
-
スライス(動的配列)の「値渡し」への移行:
src/cmd/gc/sys.goおよびsrc/cmd/gc/sysimport.cにおいて、sys.newarray,sys.arraysliced,sys.arrayslices,sys.arrays2dといったランタイム関数のシグネチャが変更されました。具体的には、これらの関数が配列(スライス)を引数として受け取る際や、戻り値として返す際に、ポインタ型 (*[]any) ではなく値型 ([]any) を使用するようになりました。- これは非常に重要な変更です。Goのスライスは、そのヘッダ(ドープベクタ)自体は値型であり、関数に渡される際にはこのヘッダがコピーされます。しかし、ヘッダ内の基底配列へのポインタはコピーされるものの、そのポインタが指す基底配列のデータは共有されます。この変更により、Goのスライスのセマンティクスがより明確になり、C言語のポインタ渡しとは異なる、Goらしい振る舞いが実現されました。
-
コンパイラの型システムとコード生成の適応:
- 新しい型チェックヘルパー関数:
src/cmd/gc/go.hとsrc/cmd/gc/subr.cで、issarray(is static array) とisdarray(is dynamic array) という新しいヘルパー関数が導入されました。これらは、コンパイル時に型が固定長配列 ([N]T) なのか、それともスライス ([]T) なのかを区別するために使用されます。 cgen.cとwalk.cの変更:isptrarrayがisptrsarrayに、isptrdarrayがisdarrayに置き換えられるなど、型チェックロジックが新しい型ヘルパー関数を使用するように更新されました。LEN(長さ) とCAP(容量) の組み込み関数が、動的配列(スライス)に対して正しく動作するようにコード生成ロジックが修正されました。特に、スライスヘッダ内のnelおよびcapフィールドから値を取得する処理が追加されています。newキーワードの処理が変更され、new([]T)のようなスライス作成時に、ポインタではなく直接スライスヘッダ(値)を返すようになりました。- スライス操作 (
OSLICE) のコード生成が、静的配列と動的配列の両方に対応するように修正されました。特に、arrayslicesとarrayslicedというランタイム関数を呼び出すロジックが、スライスの値渡しセマンティクスに合わせて調整されています。
dcl.cの変更: 構造体フィールドにおける「オープン配列」(サイズが未定の配列、つまりスライス)の宣言に関する制限が緩和されました。これは、スライスが構造体のフィールドとしてより自然に扱えるようになったことを意味します。
- 新しい型チェックヘルパー関数:
これらの変更は、Goのスライスが現在持つ、強力で柔軟な機能の基盤を形成しました。特に、スライスヘッダの値渡しセマンティクスは、Goのコードが直感的で効率的であるための重要な要素です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイルとコードブロックです。
-
src/cmd/gc/go.hにおけるArray構造体の定義変更:--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -69,9 +69,8 @@ typedef struct Array Array; struct Array { // must not move anything uchar array[8]; // pointer to data - uint32 nel; // number of elements - uint32 cap; // allocated number of elements - uchar b; // actual array - may not be contig + uchar nel[4]; // number of elements + uchar cap[4]; // allocated number of elements };この変更は、
Array構造体がGoのスライスヘッダ(ドープベクタ)のC言語表現であることを明確にしています。nelとcapがuint32からuchar[4]に変更されたのは、メモリレイアウトをバイト単位で厳密に制御するためです。 -
src/runtime/runtime.hにおけるArray構造体の定義変更:--- a/src/runtime/runtime.h +++ b/src/runtime/runtime.h @@ -115,7 +115,6 @@ struct Array byte* array; // actual data uint32 nel; // number of elements uint32 cap; // allocated number of elements - byte b[8]; // actual array - may not be contig };byte b[8];の削除は、Array構造体がもはや配列データを直接埋め込むのではなく、常に外部のメモリ領域を指す純粋なドープベクタであることを示しています。 -
src/cmd/gc/sys.goおよびsrc/cmd/gc/sysimport.cにおけるシステム関数のシグネチャ変更:--- a/src/cmd/gc/sys.go +++ b/src/cmd/gc/sys.go @@ -79,10 +79,10 @@ export func selectrecv(sel *byte, hchan *chan any, elem *any) (selected bool); export func selectdefault(sel *byte) (selected bool); export func selectgo(sel *byte); -export func newarray(nel int, cap int, width int) (ary *[]any); -export func arraysliced(old *[]any, lb int, hb int, width int) (ary *[]any); -export func arrayslices(old *any, nel int, lb int, hb int, width int) (ary *[]any); -export func arrays2d(old *any, nel int) (ary *[]any); +export func newarray(nel int, cap int, width int) (ary []any); +export func arraysliced(old []any, lb int, hb int, width int) (ary []any); +export func arrayslices(old *any, nel int, lb int, hb int, width int) (ary []any); +export func arrays2d(old *any, nel int) (ary []any);これらの変更は、ランタイム関数がスライスヘッダをポインタではなく値として受け渡し、返すようになったことを明確に示しています。これはGoのスライスのセマンティクスにおける最も重要な変更点の一つです。
-
src/runtime/array.cにおけるランタイム関数の実装変更:sys·newarray,sys·arraysliced,sys·arrayslices,sys·arrays2dの各関数で、Array* retがArray retに変更され、関数内でmal(メモリ確保) したdポインタをretに代入するのではなく、直接ret構造体のフィールドを操作するようになりました。 例えば、sys·newarrayの変更点:--- a/src/runtime/array.c +++ b/src/runtime/array.c @@ -6,23 +6,20 @@ static int32 debug = 0; -// newarray(nel uint32, cap uint32, width uint32) (ary *[]any);\n +// newarray(nel int, cap int, width int) (ary []any);\n void -sys·newarray(uint32 nel, uint32 cap, uint32 width, Array* ret)\n +sys·newarray(uint32 nel, uint32 cap, uint32 width, Array ret)\n {\n - Array *d;\n uint64 size;\n \n if(cap < nel)\n cap = nel;\n size = cap*width;\n \n - d = mal(sizeof(*d) - sizeof(d->b) + size);\n - d->nel = nel;\n - d->cap = cap;\n - d->array = d->b;\n +\tret.nel = nel;\n +\tret.cap = cap;\n +\tret.array = mal(size);\n \n - ret = d;\n FLUSH(&ret);\n \n if(debug) {これは、スライスヘッダが値として渡され、関数内でその値が直接変更されることを示しています。
-
src/cmd/gc/walk.cにおけるnewcompat関数の変更:newキーワードの処理が大幅に修正され、ポインタ型だけでなく、直接スライス型を返すように変更されています。--- a/src/cmd/gc/walk.c +++ b/src/cmd/gc/walk.c @@ -1992,45 +1992,69 @@ newcompat(Node *n)\n Type *t;\n \n t = n->type;\n - if(t == T || !isptr[t->etype] || t->type == T)\n - fatal("newcompat: type should be pointer %lT", t);\n +\tif(t == T)\n +\t\tgoto bad;\n +\n +\tif(isptr[t->etype]) {\n +\t\tif(t->type == T)\n +\t\t\tgoto bad;\n +\t\tt = t->type;\n +\n +\t\tdowidth(t);\n +\n +\t\ton = syslook("mal", 1);\n +\t\targtype(on, t);\n +\n +\t\tr = nodintconst(t->width);\n +\t\tr = nod(OCALL, on, r);\n +\t\twalktype(r, Erv);\n +\n +\t\tr->type = n->type;\n +\t\tgoto ret;\n +\t}\n \n - t = t->type;\n switch(t->etype) {\n - case TFUNC:\n - \tyyerror("cannot make new %T", t);\n - \tbreak;\n +\tdefault:\n +\t\tgoto bad;\n +\n +\tcase TSTRUCT:\n +\t\tif(n->left != N)\n +\t\t\tyyerror("dont know what new(,e) means");\n +\n +\t\tdowidth(t);\n +\n +\t\ton = syslook("mal", 1);\n +\n +\t\targtype(on, t);\n +\n +\t\tr = nodintconst(t->width);\n +\t\tr = nod(OCALL, on, r);\n +\t\twalktype(r, Erv);\n +\n +\t\tr->type = ptrto(n->type);\n +\n +\t\treturn r;\n case TMAP:\n +\t\tn->type = ptrto(n->type);\n \tr = mapop(n, Erv);\n - \treturn r;\n +\t\tbreak;\n \n case TCHAN:\n +\t\tn->type = ptrto(n->type);\n \tr = chanop(n, Erv);\n - \treturn r;\n +\t\tbreak;\n \n case TARRAY:\n \tr = arrayop(n, Erv);\n - \treturn r;\n +\t\tbreak;\n }\n \n - if(n->left != N)\n - yyerror("dont know what new(,e) means");\n -\n - dowidth(t);\n -\n - on = syslook("mal", 1);\n -\n - argtype(on, t);\n -\n - r = nodintconst(t->width);\n - r = nod(OCALL, on, r);\n - walktype(r, Erv);\n -\n -// r = nod(OCONV, r, N);\n - r->type = n->type;\n -\n +ret:\n return r;\n +\n +bad:\n +\tfatal("cannot make new %T", t);\n +\treturn n;\n }この変更は、
new([]T)のようなスライス作成が、ポインタではなく直接スライスヘッダ(値)を返すようにコンパイラの挙動を調整しています。
コアとなるコードの解説
上記のコアとなるコード変更は、Go言語のスライスが「値型」として振る舞うという現在のセマンティクスを確立する上で極めて重要です。
-
Array構造体の統一と簡素化:go.hとruntime.hにおけるArray構造体の変更は、Goのスライスが常に「ポインタ、長さ、容量」の3要素からなるドープベクタとして表現されることを保証します。bフィールドの削除は、スライスが基底配列のデータを直接保持するのではなく、常に外部のメモリ領域を参照するという設計思想を強化します。これにより、スライスのコピーは常にドープベクタのコピーとなり、基底配列のデータは共有されるという、Goの直感的なスライスセマンティクスが実現されます。 -
ランタイム関数の値渡しへの移行:
sys.goとsysimport.cでのシステム関数のシグネチャ変更、およびruntime/array.cでの実装変更は、スライスヘッダが関数間で値として渡されるようになったことを意味します。これは、Goの関数呼び出し規約と一貫性を持たせるための重要なステップです。スライスヘッダは小さいため、値渡しは効率的であり、ポインタ渡しによるエイリアシングの問題を回避し、コードの可読性と安全性を向上させます。関数内でスライスヘッダのフィールドを直接操作することで、メモリ割り当てやデータコピーのオーバーヘッドを最小限に抑えつつ、スライスの状態を効率的に更新できます。 -
コンパイラのセマンティクス適応:
walk.cやcgen.cにおける変更は、コンパイラが新しいスライスのセマンティクスを正しく理解し、適切なコードを生成できるようにするためのものです。特に、newキーワードがスライスに対して使用された際に、ポインタではなくスライスヘッダの値が返されるようになったことは、Goのコードがより自然に記述できるようになる上で不可欠です。また、LENやCAPといった組み込み関数がスライスヘッダのフィールドを直接参照するようになったことで、これらの操作が非常に効率的に行われるようになりました。新しい型ヘルパー関数 (issarray,isdarray) の導入は、コンパイラが異なる種類の配列(固定長配列とスライス)を正確に区別し、それぞれに適切な処理を適用するための基盤を提供します。
これらの変更は相互に関連しており、Goのスライスが現在持つ、強力で直感的、かつ効率的な機能の基盤を形成しています。
関連リンク
- Go言語の初期設計に関する議論やドキュメントは、Goの公式リポジトリの初期コミットや、Goの設計に関するブログ記事、メーリングリストのアーカイブなどで見つけることができます。
- Go Slices: usage and internals: https://go.dev/blog/slices-intro (このコミットより後の記事ですが、スライスの内部構造を理解する上で非常に役立ちます)
- Go Data Structures: Interfaces, Arrays, and Slices: https://go.dev/blog/go-data-structures (これも後の記事ですが、スライスの概念を深く理解するのに役立ちます)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
src/cmd/gc,src/runtimeディレクトリの初期バージョン) - Gitのコミットログと差分表示
- Go言語のブログ記事や設計に関する議論(Goの歴史的背景を理解するため)