[インデックス 15102] ファイルの概要
コミット
commit 2af3cbe308986005715bed3fa8ec5975e32ea7b7
Author: Russ Cox <rsc@golang.org>
Date: Sat Feb 2 23:54:21 2013 -0500
cmd/gc: treat &T{} like new(T): allocate on stack if possible
Fixes #4618.
R=ken2
CC=golang-dev
https://golang.org/cl/7278048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2af3cbe308986005715bed3fa8ec5975e32ea7b7
元コミット内容
このコミットは、Goコンパイラ(cmd/gc
)において、複合リテラル(&T{}
)の扱いを改善し、new(T)
と同様に可能な限りスタックにアロケートするように変更するものです。これにより、ヒープアロケーションを減らし、パフォーマンスを向上させることを目的としています。この変更は、Go issue #4618 を修正します。
変更の背景
Go言語では、メモリ管理はガベージコレクションによって自動的に行われます。しかし、ヒープへのアロケーションは、ガベージコレクタの負荷を増やし、プログラムの実行速度に影響を与える可能性があります。そのため、コンパイラは可能な限りオブジェクトをスタックにアロケートする「エスケープ解析(Escape Analysis)」という最適化を行います。スタックにアロケートされたオブジェクトは、関数が終了すると自動的に解放されるため、ガベージコレクションの対象外となり、パフォーマンスが向上します。
Go 1.0のリリース当初、new(T)
はエスケープ解析の対象となっており、オブジェクトが関数スコープ内で完結し、外部にエスケープしない場合はスタックにアロケートされていました。しかし、&T{}
のような複合リテラルは、常にヒープにアロケートされるという挙動を示していました。これは、new(T)
と&T{}
がセマンティクス的には非常に似ているにもかかわらず、異なるアロケーション戦略を取るという一貫性のない挙動であり、開発者にとっては混乱の元となる可能性がありました。
issue #4618では、この&T{}
が常にヒープアロケーションされる問題が報告されており、特に短い寿命のオブジェクトが頻繁に生成される場合に、不要なヒープアロケーションとそれに伴うガベージコレクションのオーバーヘッドが発生していました。このコミットは、この問題を解決し、&T{}
もnew(T)
と同様にエスケープ解析の恩恵を受けられるようにすることで、Goプログラムの実行効率を向上させることを目的としています。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。ソースコードを機械語に変換するだけでなく、様々な最適化を行います。 - エスケープ解析 (Escape Analysis): コンパイラが行う最適化の一つで、変数がヒープにアロケートされるべきか、それともスタックにアロケートできるかを決定します。
- スタックアロケーション: 関数呼び出し時に確保され、関数終了時に自動的に解放されるメモリ領域。高速で、ガベージコレクションの対象外です。
- ヒープアロケーション: プログラムの実行中に動的に確保されるメモリ領域。ガベージコレクタによって管理され、不要になったオブジェクトはガベージコレクションによって解放されます。スタックアロケーションに比べてオーバーヘッドがあります。
new(T)
: 型T
のゼロ値へのポインタを返します。例えば、new(int)
は*int
型の値を返し、その値は0
に初期化されています。- 複合リテラル (
&T{}
): 構造体や配列、スライスなどの複合型を初期化するための構文です。&T{}
は、型T
の新しいインスタンスを作成し、そのアドレス(ポインタ)を返します。例えば、&struct{i int}{i: 10}
は、i
フィールドが10
に初期化された匿名構造体へのポインタを返します。 - AST (Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、それに対して様々な解析や最適化を行います。
ONew
: Goコンパイラの内部表現における、new
演算子を表すASTノードです。OAS
: Goコンパイラの内部表現における、代入演算子(=
)を表すASTノードです。callnew
:new
演算子に対応するランタイム関数を呼び出すためのコンパイラ内部のヘルパー関数です。
技術的詳細
このコミットの核心は、Goコンパイラのsrc/cmd/gc/sinit.c
ファイル内のanylit
関数における変更です。anylit
関数は、複合リテラルを含む初期化処理を担当しています。
変更前は、&T{}
のような複合リテラルがポインタ型である場合、callnew(t->type)
を呼び出して新しいオブジェクトをヒープにアロケートしていました。callnew
は、Goランタイムのnew
関数に対応するもので、常にヒープアロケーションを行います。
変更後は、&T{}
の処理において、直接callnew
を呼び出すのではなく、まずONew
ノードを明示的に作成するように変更されています。具体的には、以下のステップが追加されています。
r = nod(ONEW, N, N);
:ONew
オペレーションを表す新しいASTノードr
を作成します。これは、new(T)
のAST表現と本質的に同じです。r->typecheck = 1;
: このノードが型チェック済みであることをマークします。r->type = t;
:ONew
ノードの型を、複合リテラルの型(ポインタが指す基底型)に設定します。r->esc = n->esc;
: 複合リテラルノードのエスケープ情報(n->esc
)をONew
ノードにコピーします。このエスケープ情報が、エスケープ解析においてスタックアロケーションが可能かどうかを判断する上で非常に重要になります。walkexpr(&r, init);
: 新しく作成されたONew
ノードr
に対して、コンパイラのウォーク処理(ASTの走査と変換)を実行します。このwalkexpr
の過程で、エスケープ解析が実行され、r
が指すオブジェクトがスタックにアロケート可能であれば、そのように決定されます。
この変更により、&T{}
はnew(T)
と全く同じASTノード構造とエスケープ解析のパスを通るようになります。結果として、&T{}
で作成されたオブジェクトも、new(T)
で作成されたオブジェクトと同様に、エスケープ解析の結果に基づいてスタックにアロケートされる可能性が生まれます。
test/fixedbugs/issue4618.go
という新しいテストファイルが追加されており、この変更が正しく機能することを確認しています。このテストでは、testing.AllocsPerRun
関数を使用して、&T{}
がヒープアロケーションを引き起こす回数を計測しています。
F()
関数では、t := &T{}
で作成されたオブジェクトがグローバル変数globl
に代入されるため、エスケープ解析によってヒープにエスケープすると判断され、1回のアロケーションが発生することが期待されます。G()
関数では、t := &T{}
で作成されたオブジェクトが関数スコープ内で完結し、外部にエスケープしないため、スタックにアロケートされ、0回のアロケーションが発生することが期待されます。
このテストは、変更が意図通りに機能し、不要なヒープアロケーションが削減されることを検証しています。
コアとなるコードの変更箇所
src/cmd/gc/sinit.c
の anylit
関数内:
--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -953,7 +953,7 @@ void
anylit(int ctxt, Node *n, Node *var, NodeList **init)
{
Type *t;
- Node *a, *vstat;
+ Node *a, *vstat, *r;
t = n->type;
switch(n->op) {
@@ -964,7 +964,14 @@ anylit(int ctxt, Node *n, Node *var, NodeList **init)
if(!isptr[t->etype])
fatal("anylit: not ptr");
- a = nod(OAS, var, callnew(t->type));
+ r = nod(ONEW, N, N);
+ r->typecheck = 1;
+ r->type = t;
+ r->esc = n->esc;
+ walkexpr(&r, init);
+
+ a = nod(OAS, var, r);
+
typecheck(&a, Etop);
*init = list(*init, a);
test/fixedbugs/issue4618.go
の新規追加:
// run
// Copyright 2013 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.
package main
import (
"fmt"
"os"
"testing"
)
type T struct { int }
var globl *T
func F() {
t := &T{}
globl = t
}
func G() {
t := &T{}
_ = t
}
func main() {
nf := testing.AllocsPerRun(100, F)
ng := testing.AllocsPerRun(100, G)
if int(nf) != 1 {
fmt.Printf("AllocsPerRun(100, F) = %v, want 1\n", nf)
os.Exit(1)
}
if int(ng) != 0 {
fmt.Printf("AllocsPerRun(100, G) = %v, want 0\n", ng)
os.Exit(1)
}
}
コアとなるコードの解説
src/cmd/gc/sinit.c
の変更は、anylit
関数がポインタ型の複合リテラル(例: &T{}
)を処理する部分にあります。
変更前は、a = nod(OAS, var, callnew(t->type));
の行で、callnew
関数が直接呼び出されていました。callnew
は、Goランタイムのnew
関数に対応するもので、これは常にヒープアロケーションを強制します。
変更後は、このcallnew
の直接呼び出しが削除され、代わりに以下のコードが挿入されています。
r = nod(ONEW, N, N);
r->typecheck = 1;
r->type = t;
r->esc = n->esc;
walkexpr(&r, init);
a = nod(OAS, var, r);
r = nod(ONEW, N, N);
: これは、GoコンパイラのASTノードを作成する関数nod
を使って、ONEW
(new
演算子)を表す新しいノードr
を生成しています。N
はnilノードを意味します。r->typecheck = 1;
: このノードが型チェック済みであることをコンパイラに伝えます。r->type = t;
:ONEW
ノードが生成するオブジェクトの型を、元の複合リテラルの型(ポインタが指す基底型)に設定します。r->esc = n->esc;
: ここが最も重要な変更点の一つです。元の複合リテラルノードn
が持っていたエスケープ情報(n->esc
)を、新しく作成されたONEW
ノードr
にコピーしています。エスケープ解析は、このesc
フィールドの情報を基に、オブジェクトがスタックにアロケートできるかどうかを判断します。walkexpr(&r, init);
: この関数呼び出しは、新しく作成されたONEW
ノードr
に対して、コンパイラのASTウォーク処理を実行します。このウォーク処理の過程で、エスケープ解析が実行され、r
が指すオブジェクトがスタックにアロケート可能であれば、そのようにマークされます。
この一連の変更により、&T{}
は内部的にnew(T)
と同じAST表現に変換され、エスケープ解析の対象となります。これにより、オブジェクトが関数スコープ内で完結し、外部にエスケープしない場合には、ヒープではなくスタックにアロケートされるようになります。
test/fixedbugs/issue4618.go
は、この変更の動作を検証するためのテストケースです。
F()
関数では、&T{}
で作成されたt
がグローバル変数globl
に代入されるため、t
は関数スコープを超えて「エスケープ」します。したがって、エスケープ解析はt
をヒープにアロケートすると判断し、AllocsPerRun
は1回のアロケーションを報告します。G()
関数では、&T{}
で作成されたt
が_ = t
という形で使用されるだけで、関数スコープ外にはエスケープしません。この変更により、エスケープ解析はt
をスタックにアロケートできると判断し、AllocsPerRun
は0回のアロケーションを報告します。
このテストは、&T{}
がnew(T)
と同様に、エスケープ解析の恩恵を受けられるようになったことを明確に示しています。
関連リンク
- Go issue #4618: https://github.com/golang/go/issues/4618
- Go CL 7278048: https://golang.org/cl/7278048
参考にした情報源リンク
- Go issue #4618 の議論
- Go言語の公式ドキュメント(エスケープ解析に関する情報)
- Goコンパイラのソースコード(
src/cmd/gc/sinit.c
およびsrc/cmd/gc/walk.c
など) - Go言語のメモリ管理とエスケープ解析に関する技術記事