Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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ノードを明示的に作成するように変更されています。具体的には、以下のステップが追加されています。

  1. r = nod(ONEW, N, N);: ONewオペレーションを表す新しいASTノードrを作成します。これは、new(T)のAST表現と本質的に同じです。
  2. r->typecheck = 1;: このノードが型チェック済みであることをマークします。
  3. r->type = t;: ONewノードの型を、複合リテラルの型(ポインタが指す基底型)に設定します。
  4. r->esc = n->esc;: 複合リテラルノードのエスケープ情報(n->esc)をONewノードにコピーします。このエスケープ情報が、エスケープ解析においてスタックアロケーションが可能かどうかを判断する上で非常に重要になります。
  5. 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.canylit 関数内:

--- 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);
  1. r = nod(ONEW, N, N);: これは、GoコンパイラのASTノードを作成する関数nodを使って、ONEWnew演算子)を表す新しいノードrを生成しています。Nはnilノードを意味します。
  2. r->typecheck = 1;: このノードが型チェック済みであることをコンパイラに伝えます。
  3. r->type = t;: ONEWノードが生成するオブジェクトの型を、元の複合リテラルの型(ポインタが指す基底型)に設定します。
  4. r->esc = n->esc;: ここが最も重要な変更点の一つです。元の複合リテラルノードnが持っていたエスケープ情報(n->esc)を、新しく作成されたONEWノードrにコピーしています。エスケープ解析は、このescフィールドの情報を基に、オブジェクトがスタックにアロケートできるかどうかを判断します。
  5. 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 の議論
  • Go言語の公式ドキュメント(エスケープ解析に関する情報)
  • Goコンパイラのソースコード(src/cmd/gc/sinit.c および src/cmd/gc/walk.c など)
  • Go言語のメモリ管理とエスケープ解析に関する技術記事