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

[インデックス 17111] ファイルの概要

このコミットは、Go言語のreflectパッケージにおけるメモリ割り当ての最適化に関するものです。具体的には、reflect.Value.Interface()メソッドが、インターフェースの内容がアドレス可能でない場合に発生していた不要なメモリ割り当てを削減します。この変更により、特に大きな構造体など、非アドレス可能な値に対するInterface()呼び出しのパフォーマンスが向上します。

変更されたファイルは以下の通りです。

  • src/pkg/reflect/all_test.go: ベンチマークとテストケースが追加されました。
  • src/pkg/reflect/value.go: Value.Interface()メソッドの実装が変更されました。

コミット

commit 94179d61abf9516e24597ec8fa3888343d01388c
Author: Rob Pike <r@golang.org>
Date:   Fri Aug 9 10:49:01 2013 +1000

    reflect: avoid allocation when interface's contents are not addressable
    See issue 4949 for a full explanation.
    
    Allocs go from 1 to zero in the non-addressable case.
    Fixes #4949.
    
    BenchmarkInterfaceBig             90           14  -84.01%
    BenchmarkInterfaceSmall           14           14   +0.00%
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12646043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/94179d61abf9516e24597ec8fa3888343d01388c

元コミット内容

reflect: avoid allocation when interface's contents are not addressable
See issue 4949 for a full explanation.

Allocs go from 1 to zero in the non-addressable case.
Fixes #4949.

BenchmarkInterfaceBig             90           14  -84.01%
BenchmarkInterfaceSmall           14           14   +0.00%

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12646043

変更の背景

このコミットは、Go言語のreflectパッケージにおけるValue.Interface()メソッドのパフォーマンス改善を目的としています。具体的には、Go issue 4949で報告された問題に対応しています。

reflect.Value.Interface()メソッドは、reflect.Value型でラップされた値を、その基となるinterface{}型に変換するために使用されます。しかし、この変換処理において、特に値がアドレス可能でない(つまり、ポインタを通じてアクセスできない)場合に、不要なメモリ割り当て(ヒープアロケーション)が発生していました。この余分な割り当ては、特にInterface()メソッドが頻繁に呼び出されるようなシナリオにおいて、ガベージコレクションの負荷を増やし、アプリケーションのパフォーマンスに悪影響を与える可能性がありました。

コミットメッセージにあるベンチマーク結果「BenchmarkInterfaceBig 90 -> 14 -84.01%」は、この最適化によって、大きな構造体などの非アドレス可能な値に対するInterface()呼び出しにおけるメモリ割り当てが大幅に削減されたことを示しています。これにより、実行速度が向上し、メモリ使用量が削減されます。

前提知識の解説

Go言語のreflectパッケージ

reflectパッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)したり、動的に値を操作したりするための機能を提供します。これにより、型情報や値の情報を実行時に取得・変更することが可能になります。

  • reflect.Type: Goの型の情報を表します。
  • reflect.Value: Goの実行時の値を表します。reflect.ValueOf(x)で任意のGoの値をreflect.Valueに変換できます。
  • Value.Interface()メソッド: reflect.Valueでラップされた値を、その基となるinterface{}型に変換して返します。このメソッドは、リフレクションを通じて取得した値を通常のGoの型として扱いたい場合に利用されます。

アドレス可能性 (Addressability)

Go言語において、値が「アドレス可能」であるとは、その値がメモリ上の特定のアドレスに存在し、そのアドレス(ポインタ)を取得できることを意味します。例えば、変数や構造体のフィールドはアドレス可能です。一方、マップの要素やインターフェースの値のコピーなどは、直接アドレス可能ではない場合があります。

reflectパッケージでは、Value.CanAddr()メソッドを使って、reflect.Valueがアドレス可能かどうかを判定できます。アドレス可能なreflect.Valueからは、Value.Addr()メソッドを使ってその値へのポインタ(reflect.Value型)を取得できます。

メモリ割り当て (Memory Allocation)

Goプログラムでは、変数は主にスタックまたはヒープに割り当てられます。

  • スタック (Stack): 関数呼び出しやローカル変数が一時的に格納されるメモリ領域です。高速にアクセスでき、関数が終了すると自動的に解放されます。
  • ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てる領域です。スタックよりもアクセスは遅く、ガベージコレクタによって不要になったメモリが解放されます。

不要なヒープ割り当ては、ガベージコレクションの頻度を増やし、プログラムのパフォーマンスを低下させる原因となります。

interface{}の内部表現

Goのinterface{}型は、内部的には2つのワードで構成されています。

  1. 型情報 (type word): インターフェースが保持している具体的な値の型情報(_type構造体へのポインタ)。
  2. 値情報 (data word): インターフェースが保持している具体的な値そのもの、またはその値へのポインタ。

値がポインタサイズよりも大きい場合や、アドレス可能でない場合は、ヒープにコピーが作成され、そのコピーへのポインタが値情報として格納されることがあります。これが、今回のコミットで最適化の対象となった部分です。

技術的詳細

このコミットの核心は、reflect.Value.Interface()メソッドが、インターフェースの内部表現を構築する際に、不要なヒープ割り当てを避けるように変更された点にあります。

以前の実装では、reflect.ValueflagIndirフラグ(間接参照されていることを示すフラグ)を持っている場合、かつ値のサイズがポインタサイズよりも大きい場合に、無条件に値のコピーを作成し、そのコピーへのポインタをインターフェースの値情報として格納していました。

しかし、flagIndirがセットされていても、その値が実際にはアドレス可能ではない(つまり、Value.CanAddr()falseを返す)場合、その値は既にヒープ上に存在しているか、あるいはスタック上にあるがポインタを通じてアクセスできない一時的な値である可能性があります。このような場合、さらにコピーを作成することは冗長であり、不要なメモリ割り当てを引き起こします。

新しい実装では、flagIndirの代わりにflagAddrフラグ(アドレス可能であることを示すフラグ)を使用するように条件が変更されました。

  • 変更前: if v.flag&flagIndir != 0 && v.typ.size > ptrSize
  • 変更後: if v.flag&flagAddr != 0 && v.typ.size > ptrSize

この変更により、Value.Interface()は、値がアドレス可能であり、かつそのサイズがポインタサイズよりも大きい場合にのみ、値のコピーを作成するようになります。

  • 値がアドレス可能でない場合: 値は既にヒープ上に存在するか、コピーが不要な形式で扱えるため、新たなコピーは作成されません。
  • 値がポインタサイズ以下の場合: 値は直接インターフェースの値情報に格納できるため、コピーは不要です。

この修正により、特に構造体などの大きな値がreflect.Valueとして扱われ、その後Interface()メソッドでinterface{}に変換される際に、不要なヒープ割り当てが回避され、パフォーマンスが向上します。コミットメッセージのベンチマーク結果が示すように、BenchmarkInterfaceBigの割り当てが大幅に削減されたのはこのためです。

コアとなるコードの変更箇所

src/pkg/reflect/all_test.go

このファイルには、reflect.Value.Interface()メソッドのメモリ割り当てをテストするための新しいベンチマークとテストケースが追加されました。

type S struct {
	i1 int64
	i2 int64
}

func BenchmarkInterfaceBig(b *testing.B) {
	v := ValueOf(S{})
	for i := 0; i < b.N; i++ {
		v.Interface()
	}
	b.StopTimer()
}

func TestAllocsInterfaceBig(t *testing.T) {
	v := ValueOf(S{})
	if allocs := testing.AllocsPerRun(100, func() { v.Interface() }); allocs > 0 {
		t.Errorf("allocs:", allocs)
	}
}

func BenchmarkInterfaceSmall(b *testing.B) {
	v := ValueOf(int64(0))
	for i := 0; i < b.N; i++ {
		v.Interface()
	}
}

func TestAllocsInterfaceSmall(t *testing.T) {
	v := ValueOf(int64(0))
	if allocs := testing.AllocsPerRun(100, func() { v.Interface() }); allocs > 0 {
		t.Errorf("allocs:", allocs)
	}
}
  • S構造体: 2つのint64フィールドを持つ、ポインタサイズよりも大きな構造体です。
  • BenchmarkInterfaceBig / TestAllocsInterfaceBig: S{}のような大きな非アドレス可能な値に対するInterface()呼び出しのパフォーマンスとメモリ割り当てを測定します。
  • BenchmarkInterfaceSmall / TestAllocsInterfaceSmall: int64(0)のような小さな値に対するInterface()呼び出しのパフォーマンスとメモリ割り当てを測定します。

TestAllocsInterfaceBigでは、S{}のような非アドレス可能な大きな値に対してInterface()を呼び出した際のメモリ割り当てが0になることを期待しています。

src/pkg/reflect/value.go

valueInterface関数(Value.Interface()メソッドの内部実装)の条件式が変更されました。

--- a/src/pkg/reflect/value.go
+++ b/src/pkg/reflect/value.go
@@ -1004,7 +1004,8 @@ func valueInterface(v Value, safe bool) interface{} {
 	eface.typ = v.typ
 	eface.word = v.iword()
 
-	if v.flag&flagIndir != 0 && v.typ.size > ptrSize {
+	// Don't need to allocate if v is not addressable or fits in one word.
+	if v.flag&flagAddr != 0 && v.typ.size > ptrSize {
 		// eface.word is a pointer to the actual data,
 		// which might be changed.  We need to return
 		// a pointer to unchanging data, so make a copy.

変更された行は以下の通りです。

- if v.flag&flagIndir != 0 && v.typ.size > ptrSize { + // Don't need to allocate if v is not addressable or fits in one word. + if v.flag&flagAddr != 0 && v.typ.size > ptrSize {

コアとなるコードの解説

この変更の肝は、条件式がv.flag&flagIndir != 0からv.flag&flagAddr != 0に変わった点です。

  • flagIndir: reflect.Valueが間接参照(ポインタを介してアクセスされる)されていることを示すフラグです。例えば、reflect.ValueOf(&x).Elem()のようにポインタから要素を取得した場合にセットされます。しかし、このフラグがセットされていても、その値自体がアドレス可能であるとは限りません(例: map[key]valuevalueflagIndirがセットされることがあるが、アドレス可能ではない)。
  • flagAddr: reflect.Valueがアドレス可能であることを示すフラグです。つまり、Value.Addr()メソッドを呼び出してその値へのポインタを取得できることを意味します。

以前のコードでは、flagIndirがセットされている場合、eface.word(インターフェースの値情報)が実際のデータへのポインタであり、そのデータが変更される可能性があるため、不変のデータを返すためにコピーを作成していました。しかし、flagIndirがセットされていても、その値がアドレス可能でない場合(例えば、一時的な値やマップの要素など)、その値は既にヒープ上に存在しているか、あるいはコピーを作成する必要がない状況でした。このような場合に無条件にコピーを作成すると、不要なヒープ割り当てが発生していました。

新しいコードでは、flagAddrがセットされている場合にのみコピーを作成するようになりました。

  • v.flag&flagAddr != 0: reflect.Valueがアドレス可能である場合。
  • v.typ.size > ptrSize: 値のサイズがポインタサイズよりも大きい場合。

この2つの条件が同時に満たされる場合にのみ、値のコピーが作成されます。これにより、以下のようなケースで不要な割り当てが回避されます。

  1. 値がアドレス可能でない場合: 例えば、reflect.ValueOf(100).Interface()のようにリテラル値をreflect.Valueに変換し、Interface()を呼び出す場合、100はアドレス可能ではありません。以前はflagIndirがセットされることがあり、不要なコピーが発生する可能性がありましたが、flagAddrはセットされないため、コピーは作成されません。
  2. 値がポインタサイズ以下の場合: int64のような値はポインタサイズよりも大きいですが、int32のような値はポインタサイズ以下の場合があります。ポインタサイズ以下の値は、インターフェースのdata wordに直接格納できるため、コピーは不要です。この条件は以前から存在しており、引き続き有効です。

この変更により、reflect.Value.Interface()がより効率的に動作し、特にリフレクションを多用するアプリケーションにおいて、メモリ使用量とガベージコレクションの負荷を軽減する効果があります。

関連リンク

参考にした情報源リンク

  • Go issue 4949: reflect: possible saved allocation in Value.Interface - GitHub: https://github.com/golang/go/issues/4949
  • Go issue 71349: reflect: allow for stack allocation of underlying value when using reflect.Value.Interface - GitHub: https://github.com/golang/go/issues/71349
  • Go言語のreflectパッケージに関する一般的な知識
  • Go言語のメモリ管理(スタックとヒープ)に関する一般的な知識
  • Go言語のインターフェースの内部表現に関する一般的な知識