[インデックス 14912] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における構造体のパディング領域やブランクフィールド(_
で宣言されたフィールド)のハッシュ計算および比較処理に関するバグ修正を扱っています。具体的には、これらの領域が構造体の等価性チェックやハッシュ値の計算に不適切に影響を与えないようにする変更が加えられています。
コミット
commit 2ad57b45833cb7db3f5ae501d97b731ef16e8ff6
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Fri Jan 18 18:26:43 2013 +0100
cmd/gc: don't hash nor compare struct padding or blank fields.
Fixes #4585.
R=rsc, golang-dev
CC=golang-dev
https://golang.org/cl/7142052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ad57b45833cb7db3f5ae501d97b731ef16e8ff6
元コミット内容
cmd/gc: don't hash nor compare struct padding or blank fields.
Fixes #4585.
R=rsc, golang-dev
CC=golang-dev
https://golang.org/cl/7142052
変更の背景
このコミットは、Go言語のIssue 4585「comparisons and hashes process blank fields and padding in structs.」を修正するために行われました。
Go言語において、構造体はメモリ上に特定のレイアウトで配置されます。この際、アライメント要件を満たすために、フィールド間に「パディング」と呼ばれる未使用の領域が挿入されることがあります。また、Goには「ブランク識別子」(_
)を使用して、変数を宣言するもののその値を意図的に無視する、あるいはフィールド名を指定しない「ブランクフィールド」という概念があります。
問題は、Goコンパイラ(cmd/gc
)が構造体の等価性比較(==
演算子)やハッシュ値の計算(マップのキーとして使用される場合など)を行う際に、これらのパディング領域やブランクフィールドの内容を誤って考慮に入れてしまっていた点にありました。
具体的には、
- パディング領域: パディング領域の内容は未定義であり、構造体のインスタンスが作成されるたびに異なるガベージ値が含まれる可能性があります。もし比較やハッシュ計算にパディングが含まれると、同じ論理的な内容を持つ構造体でも、パディング領域の偶然の値の違いによって異なるものと判断されたり、異なるハッシュ値が生成されたりする問題が発生します。これは、構造体の等価性やマップの動作に非決定性をもたらし、予期せぬバグの原因となります。
- ブランクフィールド: ブランクフィールドは、プログラマがそのフィールドの値を意図的に無視することを示します。したがって、その内容が構造体の論理的な等価性やハッシュ値に影響を与えるべきではありません。しかし、以前の実装では、これらのフィールドも比較やハッシュの対象に含まれてしまう可能性がありました。
この問題は、特に構造体をマップのキーとして使用する場合に顕著に現れます。マップはキーのハッシュ値と等価性比較に基づいて動作するため、パディングやブランクフィールドが考慮されると、同じ論理的なキーが異なるものとして扱われ、マップが正しく機能しなくなる恐れがありました。
このコミットは、これらの問題を解決し、構造体の比較とハッシュ計算が、その論理的な内容(つまり、明示的に宣言された非ブランクフィールドの値)のみに基づいて行われるようにすることで、Goプログラムの堅牢性と予測可能性を向上させることを目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語およびコンパイラに関する前提知識が必要です。
-
Go言語の構造体 (Structs):
- Goの構造体は、異なる型のフィールドをまとめた複合データ型です。
- 構造体のゼロ値: 構造体のフィールドは、明示的に初期化されない場合、それぞれの型のゼロ値で初期化されます(例:
int
は0
、string
は""
、ポインタはnil
)。 - 構造体の比較: Goでは、構造体のすべてのフィールドが比較可能であれば、
==
演算子を使って構造体同士を比較できます。比較はフィールドごとに再帰的に行われます。 - 構造体のハッシュ: 構造体がマップのキーとして使用される場合、その構造体のハッシュ値が計算されます。ハッシュ値は、構造体の内容に基づいて一意に生成されるべきです。
-
メモリレイアウトとパディング (Memory Layout and Padding):
- コンピュータのメモリはバイト単位でアドレス指定されますが、CPUは通常、特定のバイト境界(アライメント)に配置されたデータに効率的にアクセスします。
- Goコンパイラは、構造体のフィールドをメモリに配置する際に、各フィールドの型のアライメント要件を満たすようにします。これにより、フィールド間に未使用のバイト(パディング)が挿入されることがあります。
- 例:
struct { A int16; B int64; C int16 }
のような構造体では、int16
の後にint64
が続く場合、int16
の後にパディングが挿入され、int64
が適切なアライメントで配置されることがあります。このパディング領域の内容は、通常、未定義(ガベージ値)です。
-
ブランク識別子 (Blank Identifier
_
):- Goでは、
_
は「ブランク識別子」と呼ばれ、値を意図的に破棄するために使用されます。 - 変数宣言:
_ = someValue
のように、値を代入するがその変数を使用しないことを示します。 - 構造体フィールド:
type S struct { A int; _ string; B float64 }
のように、構造体フィールド名として_
を使用できます。これは、そのフィールドがメモリを占有するものの、プログラムからはアクセスされず、その値が構造体の論理的な内容の一部ではないことを示します。
- Goでは、
-
Goコンパイラ (
cmd/gc
) の内部構造:cmd/gc
はGo言語の公式コンパイラの一つで、Goソースコードを機械語に変換します。- AST (Abstract Syntax Tree): コンパイラはソースコードを解析し、ASTと呼ばれるツリー構造を構築します。
- 型システム (Type System): コンパイラは型の情報を管理し、型チェックやメモリレイアウトの決定を行います。
- コード生成 (Code Generation): 比較やハッシュ計算のような操作は、コンパイラによって適切な機械語命令やランタイム関数呼び出しに変換されます。
algtype1
関数: 型が比較可能かどうか、どのように比較されるべきかを決定するコンパイラ内部の関数。AMEM
はメモリ比較で十分であることを示し、ANOEQ
は比較不可能であることを示します。genhash
関数: 構造体のハッシュ関数を生成するコンパイラ内部の関数。geneq
関数: 構造体の等価性比較関数を生成するコンパイラ内部の関数。walkcompare
関数: 比較演算子(==
など)のASTノードを処理し、コード生成の準備をする関数。
これらの知識を前提として、このコミットがGoコンパイラの内部でどのように構造体の比較とハッシュのロジックを変更し、パディングとブランクフィールドの問題を解決したかを理解することができます。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのcmd/gc
が構造体の比較(equality)とハッシュ(hashing)をどのように処理するか、そしてその処理からパディングとブランクフィールドをどのように除外するかという点に集約されます。
Goコンパイラは、構造体の比較やハッシュ計算のために、特定の型に対して特別な関数を生成したり、メモリ比較(memequal
やmemhash
)を利用したりします。このコミットの目的は、これらの処理において、構造体の論理的な内容に寄与しないメモリ領域(パディング)や、プログラマが意図的に無視するフィールド(ブランクフィールド)が考慮されないようにすることです。
主要な変更点は以下の通りです。
-
ispaddedfield
関数の導入:src/cmd/gc/subr.c
に新しくispaddedfield(Type *t)
関数が追加されました。- この関数は、与えられた構造体フィールドの型情報(
Type *t
)を基に、そのフィールドの直後にパディングが存在するかどうかを判定します。 - 具体的には、フィールドの幅(
t->width
)とフィールドの型の幅(t->type->width
)の合計が、次のフィールドまでのオフセット(t->down->width
)と一致しない場合にパディングがあると判断します。これは、フィールドがメモリ上で連続して配置されず、間に隙間があることを意味します。
-
algtype1
関数の変更:algtype1
は、ある型がどのように比較されるべきか(例: メモリ比較で十分か、特別な比較関数が必要か、比較不可能か)を決定する関数です。- この関数内で、構造体のフィールドを走査する際に、ブランクフィールド(
isblanksym(t1->sym)
)または新しく導入されたispaddedfield(t1)
がtrue
の場合、そのフィールドは比較の対象から除外され、ret = -1
(特別な比較が必要、またはメモリ比較が不適切)が設定されます。これにより、パディングやブランクフィールドを含む構造体全体を単純なメモリ比較で処理するのを防ぎます。
-
genhash
関数の変更:genhash
は、構造体のハッシュ関数を生成する役割を担います。- 変更前は、ブランクフィールドやメモリ比較可能なフィールドの連続を
memhash
で処理しようとしていました。 - 変更後、
ispaddedfield(t1)
がtrue
の場合、たとえそのフィールドがメモリ比較可能であっても、その連続を中断し、次のフィールドから個別に処理するようにロジックが修正されました。これは、パディング領域がハッシュ計算に含まれないようにするためです。 - また、ブランクフィールドはハッシュ計算の対象から完全に除外されるようになりました(
if(isblanksym(t1->sym)) continue;
)。 - さらに、生成されるハッシュ関数が空になる可能性を考慮し、
fn->nbody = list(fn->nbody, nod(ORETURN, N, N));
という行が追加され、常にreturn
文が生成されるように保証されています。
-
geneq
関数の変更:geneq
は、構造体の等価性比較関数を生成する役割を担います。genhash
と同様に、ispaddedfield(t1)
がtrue
の場合、メモリ比較の連続を中断し、パディング領域が比較に含まれないようにロジックが修正されました。- ブランクフィールドも比較の対象から完全に除外されるようになりました(
if(isblanksym(t1->sym)) continue;
)。
-
walkcompare
関数の変更:walkcompare
は、コンパイラのASTウォークフェーズで比較演算子を処理する関数です。- 構造体のフィールドを個別に比較する際に、ブランクフィールド(
isblanksym(t1->sym)
)は比較の対象から除外されるようになりました(if(isblanksym(t1->sym)) continue;
)。これにより、インラインで比較が展開される場合でも、ブランクフィールドが無視されます。
これらの変更により、Goコンパイラは構造体の比較とハッシュ計算において、パディング領域の不定な値やブランクフィールドの意図的に無視される値を適切にスキップするようになり、構造体の論理的な内容のみに基づいてこれらの操作が行われるようになりました。これにより、Goプログラムの予測可能性と堅牢性が向上します。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/gc/subr.c
:ispaddedfield
関数の新規追加。algtype1
関数内で、ブランクフィールドとパディングフィールドを特別扱いするロジックの追加。genhash
関数内で、パディングフィールドとブランクフィールドの処理ロジックの修正。geneq
関数内で、パディングフィールドとブランクフィールドの処理ロジックの修正。
-
src/cmd/gc/walk.c
:walkcompare
関数内で、構造体フィールドのインライン比較時にブランクフィールドをスキップするロジックの追加。
-
test/blank.go
:unsafe.Pointer
を使用して構造体のパディング領域を操作し、比較が正しく行われることを確認するテストケースの追加。
-
test/fixedbugs/issue4585.go
:- Issue 4585を再現し、修正が正しく機能することを確認するための新しいテストファイル。パディングを持つ構造体
T
とブランクフィールドを持つ構造体U
、およびインライン比較されるUSmall
を使用して、等価性比較とマップのハッシュ動作を検証しています。
- Issue 4585を再現し、修正が正しく機能することを確認するための新しいテストファイル。パディングを持つ構造体
コアとなるコードの解説
src/cmd/gc/subr.c
ispaddedfield
関数の追加
+static int
+ispaddedfield(Type *t)
+{
+ if(t->etype != TFIELD)
+ fatal("ispaddedfield called non-field %T", t);
+ return t->down != T && t->width + t->type->width != t->down->width;
+}
この関数は、Type *t
が指す構造体フィールドの直後にパディングが存在するかどうかを判定します。
t->etype != TFIELD
:t
がフィールド型でない場合はエラーとします。t->down != T
: 次のフィールドが存在することを確認します。t->width + t->type->width != t->down->width
: 現在のフィールドのオフセット(t->width
)にそのフィールド自体のサイズ(t->type->width
)を加えたものが、次のフィールドのオフセット(t->down->width
)と一致しない場合、その間にパディングが存在すると判断します。
algtype1
関数の変更
for(t1=t->type; t1!=T; t1=t1->down) {
- if(isblanksym(t1->sym))
+ // Blank fields and padding must be ignored,
+ // so need special compare.
+ if(isblanksym(t1->sym) || ispaddedfield(t1)) {
+ ret = -1;
continue;
+ }
a = algtype1(t1->type, bad);
if(a == ANOEQ)
return ANOEQ; // not comparable
構造体のフィールドを走査するループ内で、isblanksym(t1->sym)
(ブランクフィールド)またはispaddedfield(t1)
(パディングを持つフィールド)の場合、ret = -1
を設定し、そのフィールドは特別な比較が必要であることを示します。これにより、これらのフィールドを含む構造体全体を単純なメモリ比較で処理するのを防ぎます。
genhash
関数の変更
for(t1=t->type;; t1=t1->down) {
- if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
- if(first == T && !isblanksym(t1->sym))
+ if(t1 != T && algtype1(t1->type, nil) == AMEM && !isblanksym(t1->sym)) {
+ if(first == T)
first = t1;
- continue;
+ // If it's a memory field but it's padded, stop here.
+ if(ispaddedfield(t1))
+ t1 = t1->down;
+ else
+ continue;
}
// Run memhash for fields up to this one.
- while(first != T && isblanksym(first->sym))
- first = first->down;
if(first != T) {
// ... (memhash生成ロジック) ...
}
if(t1 == T)
break;
+ if(isblanksym(t1->sym))
+ continue;
// Run hash for this field.
// ... (個別フィールドのハッシュ生成ロジック) ...
}
+ // make sure body is not empty.
+ fn->nbody = list(fn->nbody, nod(ORETURN, N, N));
algtype1(t1->type, nil) == AMEM && !isblanksym(t1->sym)
: メモリ比較可能で、かつブランクフィールドではないフィールドの連続を処理します。if(ispaddedfield(t1)) t1 = t1->down; else continue;
: もし現在のフィールドがパディングを持つ場合、memhash
の連続を中断し、次のフィールドから個別に処理するようにします。パディング領域はハッシュ計算に含まれません。if(isblanksym(t1->sym)) continue;
: ブランクフィールドはハッシュ計算の対象から完全にスキップされます。fn->nbody = list(fn->nbody, nod(ORETURN, N, N));
: 生成されるハッシュ関数が空になることを防ぐため、常にreturn
文を追加します。
geneq
関数の変更
genhash
と同様のロジックがgeneq
にも適用されています。
for(t1=t->type;; t1=t1->down) {
- if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
- if(first == T && !isblanksym(t1->sym))
+ if(t1 != T && algtype1(t1->type, nil) == AMEM && !isblanksym(t1->sym)) {
+ if(first == T)
first = t1;
- continue;
+ // If it's a memory field but it's padded, stop here.
+ if(ispaddedfield(t1))
+ t1 = t1->down;
+ else
+ continue;
}
// Run memequal for fields up to this one.
- while(first != T && isblanksym(first->sym))
- first = first->down;
if(first != T) {
// ... (memequal生成ロジック) ...
}
if(t1 == T)
break;
+ if(isblanksym(t1->sym))
+ continue;
// Check this field, which is not just memory.
// ... (個別フィールドの比較生成ロジック) ...
ispaddedfield(t1)
の場合、memequal
の連続を中断し、パディング領域が比較に含まれないようにします。isblanksym(t1->sym)
の場合、ブランクフィールドは比較の対象から完全にスキップされます。
src/cmd/gc/walk.c
walkcompare
関数の変更
\t\tfor(t1=t->type; t1; t1=t1->down) {
+\t\t\tif(isblanksym(t1->sym))\n+\t\t\t\tcontinue;\n \t\t\tli = nod(OXDOT, l, newname(t1->sym));
\t\t\tri = nod(OXDOT, r, newname(t1->sym));
\t\t\ta = nod(n->op, li, ri);
構造体のフィールドをインラインで比較する際に、isblanksym(t1->sym)
(ブランクフィールド)であれば、そのフィールドの比較をスキップします。
test/blank.go
type T1 struct{ x, y, z int }
t1 := *(*T)(unsafe.Pointer(&T1{1, 2, 3}))
t2 := *(*T)(unsafe.Pointer(&T1{4, 5, 6}))
if t1 != t2 {
panic("T{} != T{}")
}
このテストは、T
という構造体(おそらくパディングを含む)を定義し、unsafe.Pointer
を使って異なるガベージ値を持つT1
構造体のメモリをT
型として解釈しています。もしパディング領域が比較に含まれると、t1 != t2
がtrue
になるはずですが、修正後はfalse
となり、パニックしないことを期待しています。これは、パディングが比較から除外されていることを検証します。
test/fixedbugs/issue4585.go
この新しいテストファイルは、以下の構造体を定義しています。
T
:int16
とint64
の間にパディングが発生する可能性のある構造体。U
: ブランクフィールド_
を含む構造体。USmall
: ブランクフィールドを含み、コンパイラが比較をインライン化する可能性のある小さな構造体。
各test
関数(test1
, test2
, test3
)では、unsafe.Pointer
とcopy
を使って構造体のパディング領域やブランクフィールドに意図的に異なるガベージ値を書き込みます。その後、==
演算子による比較と、マップのキーとしてのハッシュ動作を検証しています。
例えば、test1
ではU
構造体を使用し、a
とb
のブランクフィールドに異なる内容を書き込んだ上で比較とマップへの挿入を行います。修正前であればa != b
となったり、マップのlen
が2
になったりするはずですが、修正後はブランクフィールドが無視されるため、a == b
となり、マップのlen
が1
になることを期待しています。これにより、ブランクフィールドが比較とハッシュから除外されていることを確認します。
これらの変更とテストにより、Goコンパイラは構造体の比較とハッシュ計算において、パディング領域の不定な値やブランクフィールドの意図的に無視される値を適切にスキップするようになり、構造体の論理的な内容のみに基づいてこれらの操作が行われるようになりました。
関連リンク
- Go Issue 4585: https://code.google.com/p/go/issues/detail?id=4585 (現在はGitHubに移行済み)
- Go CL 7142052: https://golang.org/cl/7142052 (Gerrit Code Reviewへのリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント (構造体、メモリレイアウト、ブランク識別子に関する一般的な情報)
- Goコンパイラのソースコード (
src/cmd/gc
ディレクトリ内の関連ファイル) - Go言語のIssueトラッカー (Issue 4585の詳細)
- Go言語のCode Reviewシステム (CL 7142052の詳細)
- Go言語のメモリレイアウトとアライメントに関する一般的な解説記事 (例: "Go's Memory Layout" などで検索)
- Go言語の構造体比較とハッシュに関する一般的な解説記事 (例: "Go struct equality", "Go struct as map key" などで検索)