[インデックス 12721] ファイルの概要
コミット
このコミットは、Goコンパイラ(cmd/gc
)におけるappend
組み込み関数のインライン展開時の引数保持に関するバグ修正です。具体的には、append
がインライン展開される際に、スライスを修正する操作が、そのスライスから読み取られる引数の値に影響を与えないようにするための変更です。これにより、append
の引数として渡されたスライス要素が、append
操作の途中で上書きされてしまう問題を解決しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/47b6197a011f201b6975407c36978dac0b1f87d7
元コミット内容
commit 47b6197a011f201b6975407c36978dac0b1f87d7
Author: Ian Lance Taylor <iant@golang.org>
Date: Thu Mar 22 09:44:31 2012 -0700
cmd/gc: when expanding append inline, preserve arguments
Fixes #3369.
R=golang-dev, gri, lvd, r
CC=golang-dev
https://golang.org/cl/5876044
---
src/cmd/gc/walk.c | 6 ++++++
test/fixedbugs/bug428.go | 19 +++++++++++++++++++
2 files changed, 25 insertions(+)
diff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c
index 5c8282b52e..ff6f1d28bd 100644
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2358,6 +2358,12 @@ append(Node *n, NodeList **init)
walkexprlistsafe(n->list, init);
+ // walkexprlistsafe will leave OINDEX (s[n]) along if both s
+ // and n are name or literal, but those may index the slice we're
+ // modifying here. Fix explicitly.
+ for(l = n->list; l; l=l->next)
+ l->n = cheapexpr(l->n, init);
+
nsrc = n->list->n;
argc = count(n->list) - 1;
if (argc < 1) {
diff --git a/test/fixedbugs/bug428.go b/test/fixedbugs/bug428.go
new file mode 100644
index 0000000000..298c455183
--- /dev/null
+++ b/test/fixedbugs/bug428.go
@@ -0,0 +1,19 @@
+// run
+
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Test that when the compiler expands append inline it does not
+// overwrite a value before it needs it (issue 3369).
+
+package main
+
+func main() {
+ s := make([]byte, 5, 6)
+ copy(s, "12346")
+ s = append(s[:len(s)-1], '5', s[len(s)-1])
+ if string(s) != "123456" {
+ panic(s)
+ }
+}
変更の背景
この変更は、Go言語の組み込み関数であるappend
がコンパイラによってインライン展開される際に発生する特定のバグを修正するために行われました。このバグは、Issue 3369として報告されており、append
の引数として渡されるスライス要素が、append
操作の途中でそのスライス自体が変更されることによって、意図しない値に上書きされてしまうというものでした。
具体的には、s = append(s[:len(s)-1], '5', s[len(s)-1])
のようなコードにおいて、s[len(s)-1]
という引数の評価が、s[:len(s)-1]
によってスライスs
が再割り当てされた後に実行されると、古いs
の最後の要素ではなく、新しい(短くなった)s
の範囲外のメモリを指してしまう可能性がありました。コンパイラがappend
をインライン展開する際、このような評価順序の依存関係を正しく処理できていなかったため、誤った結果が生じていました。
この修正の目的は、append
の引数が、append
操作によって変更される可能性のあるスライスを参照している場合でも、その引数の値が正しく評価され、保持されるようにすることです。これにより、Goプログラムの予測可能性と正確性が向上します。
前提知識の解説
Go言語のスライス (Slice)
Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライスは、内部的に配列へのポインタ、長さ(len
)、容量(cap
)の3つの要素で構成されます。
- 長さ (Length): スライスに含まれる要素の数。
len(s)
で取得できます。 - 容量 (Capacity): スライスの基底配列の先頭から、スライスが拡張できる最大要素数。
cap(s)
で取得できます。
スライスは動的なサイズ変更が可能ですが、これは新しい基底配列の割り当てと要素のコピーによって実現されます。append
関数は、スライスの容量が不足した場合に、より大きな新しい基底配列を割り当て、既存の要素をコピーし、新しい要素を追加します。このとき、元のスライス変数は新しいスライスヘッダ(新しい基底配列へのポインタを含む)を指すように更新されます。
append
組み込み関数
append
はGo言語の組み込み関数で、スライスに要素を追加するために使用されます。
newSlice = append(oldSlice, elem1, elem2, ...)
のように使用し、oldSlice
にelem1
, elem2
, ...を追加した新しいスライスを返します。
もしoldSlice
の容量が足りない場合、append
は新しい、より大きな基底配列を割り当てて、既存の要素と新しい要素をその新しい配列にコピーします。このため、append
の呼び出し元で返り値を受け取ることが重要です。
コンパイラのインライン展開 (Inlining)
インライン展開とは、コンパイラ最適化の一種で、関数呼び出しをその関数の本体のコードで直接置き換えることです。これにより、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)が削減され、プログラムの実行速度が向上する可能性があります。
Goコンパイラも、特定の条件(関数が小さい、呼び出し回数が多いなど)を満たす関数に対してインライン展開を行います。append
のような組み込み関数は、パフォーマンス上の理由から頻繁にインライン展開の対象となります。
OINDEX
ノード
Goコンパイラの内部では、プログラムのソースコードは抽象構文木(AST)として表現されます。OINDEX
は、このASTにおけるインデックス操作(例: s[n]
)を表すノードの一種です。
cheapexpr
関数
cheapexpr
はGoコンパイラの内部関数で、式を評価し、その結果を一時変数に格納するなどの方法で、式の副作用を最小限に抑えたり、評価順序を制御したりするために使用されます。特に、式が複数回評価される可能性がある場合や、評価のタイミングが重要となる場合に利用されます。この関数は、式の評価が「安価」(副作用が少ない、または制御可能)であることを保証する目的で使われます。
技術的詳細
このコミットは、Goコンパイラのsrc/cmd/gc/walk.c
ファイル内のappend
関数の処理ロジックを変更することで、Issue 3369で報告されたバグを修正しています。
バグの根本原因は、append
がインライン展開される際に、その引数リストに含まれる式(特にスライスインデックス操作OINDEX
)の評価順序が、append
操作によるスライスの再割り当てと競合する可能性があったことです。
例えば、s = append(s[:len(s)-1], '5', s[len(s)-1])
という式を考えます。
s[:len(s)-1]
が評価され、新しいスライスが生成されます。append
がインライン展開され、必要に応じて新しい基底配列が割り当てられ、要素がコピーされます。この時点で、s
変数は新しいスライスヘッダを指すように更新される可能性があります。- その後、
s[len(s)-1]
が評価されます。もしs
がステップ2で新しいスライスヘッダを指すように更新されていた場合、このs[len(s)-1]
は、元のスライスの最後の要素ではなく、新しいスライスの(おそらく範囲外の)メモリ位置を参照してしまう可能性がありました。
修正は、walkexprlistsafe
が引数リストを処理した後、特にOINDEX
ノード(s[n]
のようなインデックス操作)に対して追加の処理を行うことで実現されています。
追加されたコードは以下の通りです。
// walkexprlistsafe will leave OINDEX (s[n]) along if both s
// and n are name or literal, but those may index the slice we're
// modifying here. Fix explicitly.
for(l = n->list; l; l=l->next)
l->n = cheapexpr(l->n, init);
このコードは、append
の引数リスト(n->list
)をイテレートし、各引数式(l->n
)に対してcheapexpr
関数を適用しています。
cheapexpr
関数は、引数式を評価し、その結果を一時変数に格納するなどの処理を行います。これにより、append
操作によってスライスが再割り当てされる前に、s[len(s)-1]
のような引数式が評価され、その値が一時的に保持されることが保証されます。結果として、append
操作がスライスを変更した後でも、引数として渡された正しい値が使用されるようになります。
この修正により、append
のインライン展開が、引数の評価順序に関する潜在的な問題を回避し、より堅牢になります。
コアとなるコードの変更箇所
変更は主にsrc/cmd/gc/walk.c
ファイル内のappend
関数にあります。
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2358,6 +2358,12 @@ append(Node *n, NodeList **init)
walkexprlistsafe(n->list, init);
+ // walkexprlistsafe will leave OINDEX (s[n]) along if both s
+ // and n are name or literal, but those may index the slice we're
+ // modifying here. Fix explicitly.
+ for(l = n->list; l; l=l->next)
+ l->n = cheapexpr(l->n, init);
+
nsrc = n->list->n;
argc = count(n->list) - 1;
if (argc < 1) {
また、このバグを再現し、修正を検証するための新しいテストケースがtest/fixedbugs/bug428.go
として追加されています。
// run
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Test that when the compiler expands append inline it does not
// overwrite a value before it needs it (issue 3369).
package main
func main() {
s := make([]byte, 5, 6)
copy(s, "12346")
s = append(s[:len(s)-1], '5', s[len(s)-1])
if string(s) != "123456" {
panic(s)
}
}
コアとなるコードの解説
src/cmd/gc/walk.c
の変更
walk.c
はGoコンパイラのバックエンドの一部であり、抽象構文木(AST)を走査し、コード生成のための準備を行う「ウォーカー」の役割を担っています。append
関数は、append
組み込み関数のASTノードを処理する部分です。
追加されたループは、append
の引数リストを反復処理します。
for(l = n->list; l; l=l->next)
: これは、append
関数の引数リストを構成するNodeList
を走査するための標準的なループです。n->list
はappend
の引数ノードのリストの先頭を指します。
l->n = cheapexpr(l->n, init);
: この行が変更の核心です。
l->n
: 現在処理している引数式を表すASTノードです。cheapexpr(l->n, init)
: この関数が呼び出されます。cheapexpr
は、引数式l->n
を評価し、その結果を一時変数に格納するなどの処理を行います。init
は、初期化文のリストを構築するために使用されるポインタです。cheapexpr
は、必要に応じて一時変数の宣言や代入といった初期化文をこのリストに追加します。l->n = ...
:cheapexpr
が返した新しいノード(一時変数への参照など)で、元の引数ノードを置き換えます。
この変更により、append
の引数として渡される式(特にs[len(s)-1]
のようなスライスインデックス操作)は、append
がスライスを再割り当てする前に評価され、その値が安全に一時変数に保存されるようになります。これにより、スライスの再割り当てが行われた後でも、引数の値が正しく参照されることが保証され、バグが修正されます。
test/fixedbugs/bug428.go
のテストケース
このテストケースは、修正されたバグを具体的に再現し、修正が正しく機能することを確認するために作成されました。
s := make([]byte, 5, 6)
: 長さ5、容量6のバイトスライスs
を作成します。
copy(s, "12346")
: スライスs
に初期値"12346"をコピーします。この時点でのs
は[49 50 51 52 54]
(ASCII値)です。
s = append(s[:len(s)-1], '5', s[len(s)-1])
: この行がバグをトリガーする核心です。
s[:len(s)-1]
はs[:4]
となり、[49 50 51 52]
("1234")という新しいスライスを生成します。append
は、この[49 50 51 52]
に'5'
(ASCII 53)とs[len(s)-1]
を追加しようとします。- バグがある場合、
s[len(s)-1]
が評価される前にs
が新しいスライスヘッダを指すように更新されると、s[len(s)-1]
は元のs
の最後の要素('6'
)ではなく、誤った値を参照してしまう可能性がありました。 - 修正後、
s[len(s)-1]
はappend
がスライスを再割り当てする前に評価され、その値('6'
)が一時的に保持されます。 - 結果として、
append
は[49 50 51 52]
に'5'
と'6'
を追加し、最終的に[49 50 51 52 53 54]
("123456")というスライスが生成され、s
に代入されます。
if string(s) != "123456" { panic(s) }
: 最終的なスライスの内容が期待通り"123456"であるかを検証します。もしバグが修正されていなければ、この条件が真となり、テストは失敗します。
関連リンク
- Go Issue 3369: cmd/gc: when expanding append inline, preserve arguments
- Go CL 5876044: cmd/gc: when expanding append inline, preserve arguments
参考にした情報源リンク
- Go言語の公式ドキュメント(スライス、append関数に関する情報)
- Goコンパイラのソースコード(
src/cmd/gc/walk.c
の関連部分) - Go Issue Tracker(Issue 3369の詳細)
- Go Code Review(CL 5876044のレビューコメント)
- コンパイラのインライン展開に関する一般的な情報