[インデックス 17899] ファイルの概要
このコミットは、Goランタイムにおけるマップのキーと値のアクセス方法を、値渡しから参照渡しに変更するものです。これにより、可変長引数(vararg)C呼び出しの廃止計画を推進し、正確なスタックスキャンを容易にするとともに、大きなキーや値を持つマップ操作のパフォーマンスを向上させます。
コミット
Author: Keith Randall khr@golang.org Date: Mon Dec 2 13:05:04 2013 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3278dc158e34779eb46cd1b5a73c1d0c18602184
元コミット内容
runtime: pass key/value to map accessors by reference, not by value.
This change is part of the plan to get rid of all vararg C calls
which are a pain for getting exact stack scanning.
We allocate a chunk of zero memory to return a pointer to when a
map access doesn't find the key. This is simpler than returning nil
and fixing things up in the caller. Linker magic allocates a single
zero memory area that is shared by all (non-reflect-generated) map
types.
Passing things by reference gets rid of some copies, so it speeds
up code with big keys/values.
benchmark old ns/op new ns/op delta
BenchmarkBigKeyMap 34 31 -8.48%
BenchmarkBigValMap 37 30 -18.62%
BenchmarkSmallKeyMap 26 23 -11.28%
R=golang-dev, dvyukov, khr, rsc
CC=golang-dev
https://golang.org/cl/14794043
変更の背景
このコミットの主な背景には、Goランタイムの内部的な最適化と、ガベージコレクション(GC)の精度向上という二つの大きな目標があります。
-
可変長引数(vararg)C呼び出しの廃止: Goの初期のランタイムは、C言語で書かれており、GoコードからCランタイム関数を呼び出す際に可変長引数(
...
)を使用する箇所がありました。しかし、可変長引数を持つC関数は、引数の型や数がコンパイル時に固定されないため、Goのガベージコレクタがスタック上のポインタを正確にスキャンするのを非常に困難にします。GCがスタック上のポインタを正確に識別できないと、誤って使用中のメモリを解放したり、到達不能なメモリを解放し損ねたりするリスクが生じます。このコミットは、マップアクセス関連のC呼び出しから可変長引数を排除し、GCの精度と信頼性を向上させるための広範な計画の一部です。 -
パフォーマンスの向上: 従来のマップアクセスでは、キーや値が構造体などの大きな型である場合、それらが値渡しされるため、関数呼び出しごとにデータのコピーが発生していました。このコピー操作は、特に頻繁なマップアクセスが行われる場合に、無視できないオーバーヘッドとなります。参照渡しに変更することで、データのコピーが不要になり、ポインタの受け渡しのみで済むため、パフォーマンスが向上します。ベンチマーク結果が示すように、特に大きなキーや値を持つマップにおいて顕著な改善が見られます。
-
ゼロ値の扱いの一貫性と簡素化: マップからキーが見つからなかった場合、Goではその型のゼロ値が返されます。以前の実装では、このゼロ値の生成と返却のロジックが複雑になる可能性がありました。このコミットでは、マップアクセスがキーを見つけられなかった場合に、あらかじめ割り当てられたゼロ値メモリへのポインタを返すように変更しています。これにより、呼び出し側でのnilチェックやゼロ値の生成ロジックが簡素化され、ランタイムのコードがよりクリーンになります。このゼロ値メモリはリンカによって単一の領域として確保され、すべてのマップ型で共有されるため、メモリ効率も良いです。
前提知識の解説
Goランタイムとガベージコレクション (GC)
Goは独自のランタイムを持ち、メモリ管理(ガベージコレクションを含む)、ゴルーチン管理、スケジューリングなどを担当します。GoのGCは、主にトレース型GC(Mark-and-Sweep)を採用しており、プログラムが使用しているメモリ(ヒープ)と、スタック上のポインタをスキャンして、到達可能なオブジェクトを特定します。
- スタックスキャン: GCが正確に動作するためには、スタック上に存在するポインタを正確に識別する必要があります。これにより、GCはスタックから参照されているヒープ上のオブジェクトを「生きている」と判断し、誤って解放することを防ぎます。
- vararg C callsの問題: C言語の可変長引数関数は、呼び出し規約が複雑で、引数の型情報が実行時まで確定しないことがあります。GoのGCは、コンパイル時に生成される型情報に基づいてスタックをスキャンしますが、可変長引数C呼び出しが存在すると、その部分のスタックレイアウトを正確に把握できず、ポインタの識別が困難になります。これはGCの正確性を損なう潜在的な問題となります。
Goのマップ (map) の内部構造
Goのマップはハッシュテーブルとして実装されており、キーと値を効率的に格納・検索できるように設計されています。
- ハッシュバケット: マップのデータは、ハッシュバケットと呼ばれる小さな配列に格納されます。キーのハッシュ値に基づいて、どのバケットにデータが格納されるかが決まります。
- オーバーフローバケット: 衝突(異なるキーが同じハッシュ値を持つこと)が発生した場合、オーバーフローバケットが使用され、同じバケットに属するキーと値のペアが連結リストのように格納されます。
hmap
構造体: マップのヘッダ情報(要素数、バケットの数など)を管理する構造体です。hash_iter
構造体: マップのイテレーション(for range
ループなど)の状態を保持するための内部構造体です。この構造体には、現在のキー、値、マップの型情報、現在のバケットへのポインタなどが含まれます。
値渡しと参照渡し
- 値渡し (Pass by Value): 関数に引数を渡す際に、引数の値そのものがコピーされて渡されます。関数内で引数の値を変更しても、呼び出し元の変数の値は変わりません。大きなデータ構造(例: 構造体、配列)を値渡しすると、コピーのコストが大きくなります。
- 参照渡し (Pass by Reference): 関数に引数を渡す際に、引数のメモリ上のアドレス(ポインタ)が渡されます。関数内でポインタを通じて値を変更すると、呼び出し元の変数の値も変更されます。コピーのコストが小さく、大きなデータ構造を効率的に扱えますが、関数内で元のデータが変更される可能性があります。Goでは、明示的にポインタを渡すことで参照渡しと同様の効果を得られます。
Goのゼロ値
Goでは、変数を宣言した際に明示的に初期化しなくても、その型の「ゼロ値」で自動的に初期化されます。例えば、数値型は0、ブール型はfalse
、文字列型は空文字列""
、ポインタ、スライス、マップ、チャネルはnil
がゼロ値です。マップアクセスでキーが見つからない場合、その要素型のゼロ値が返されるのはこのGoの言語仕様に基づいています。
技術的詳細
このコミットの主要な変更点は、Goコンパイラ(src/cmd/gc
)とランタイム(src/pkg/runtime
)におけるマップアクセス関連の内部関数の引数渡し規約の変更です。
-
マップアクセサの引数変更:
runtime·mapaccess1
(単一値取得),runtime·mapaccess2
(値と存在チェック取得),runtime·mapassign1
(値の代入),runtime·mapdelete
(要素削除) といったマップ操作のランタイム関数において、キーと値の引数がany
(値渡し) から*any
(ポインタ渡し、実質的な参照渡し) に変更されました。- これにより、これらの関数が呼び出される際に、キーや値のデータそのものがコピーされるのではなく、それらへのポインタが渡されるようになります。特に大きなキーや値(例: 構造体や配列)の場合、この変更はコピーオーバーヘッドを削減し、パフォーマンスを向上させます。
-
ゼロ値メモリの導入:
- マップアクセスでキーが見つからなかった場合、以前は要素型のゼロ値が生成されて返されていました。このコミットでは、
Type
構造体(src/pkg/reflect/type.go
およびsrc/pkg/runtime/type.h
)にzero unsafe.Pointer
フィールドが追加されました。 - この
zero
フィールドは、その型に対応するゼロ値が格納されたメモリ領域へのポインタを指します。マップアクセスでキーが見つからない場合、このzero
ポインタが返されるようになりました。 - このゼロ値メモリは、リンカによって単一のグローバルな領域として確保され、すべての(リフレクションで生成されない)マップ型で共有されます。これにより、ゼロ値の生成ロジックが簡素化され、メモリ効率も向上します。
src/cmd/gc/reflect.c
のdcommontype
関数でzerovalue
シンボルが定義され、リンカがそのサイズを調整するようになっています。
- マップアクセスでキーが見つからなかった場合、以前は要素型のゼロ値が生成されて返されていました。このコミットでは、
-
コンパイラの変更:
src/cmd/gc/builtin.c
: ランタイム関数のシグネチャ定義が更新され、マップアクセサのキーと値がポインタ型になりました。src/cmd/gc/walk.c
: コンパイラのウォークフェーズ(AST変換)において、マップアクセス(OINDEXMAP
)、マップ代入(OAS
withOINDEXMAP
)、マップ削除(ODELETE
)のコード生成ロジックが変更されました。キーや値がリテラルや一時的な値である場合、一時変数に格納し、そのアドレスをランタイム関数に渡すように変換されます。これにより、常にポインタが渡されることが保証されます。src/cmd/gc/range.c
:for range
ループにおけるマップイテレーションの処理が変更されました。mapiter1
とmapiter2
といったランタイム関数が削除され、イテレータ構造体hash_iter
から直接キーと値のポインタを取得するようになりました。
-
リフレクションパッケージの変更:
src/pkg/reflect/type.go
:rtype
構造体にzero unsafe.Pointer
フィールドが追加され、ptrTo
,ChanOf
,MapOf
,SliceOf
,arrayOf
といった型生成関数でこのzero
フィールドが初期化されるようになりました。src/pkg/reflect/value.go
:Zero
関数が、型のゼロ値を取得する際に、新しく追加されたt.zero
フィールドを利用するように変更されました。
-
ベンチマークの追加と結果:
src/pkg/runtime/mapspeed_test.go
にBenchmarkBigKeyMap
,BenchmarkBigValMap
,BenchmarkSmallKeyMap
が追加されました。- コミットメッセージに記載されているように、大きなキーや値を持つマップ操作で顕著なパフォーマンス改善が見られます。
benchmark old ns/op new ns/op delta BenchmarkBigKeyMap 34 31 -8.48% BenchmarkBigValMap 37 30 -18.62% BenchmarkSmallKeyMap 26 23 -11.28%
コアとなるコードの変更箇所
src/cmd/gc/builtin.c
マップアクセス関連のランタイム関数のシグネチャが変更されています。
--- a/src/cmd/gc/builtin.c
+++ b/src/cmd/gc/builtin.c
@@ -60,20 +60,18 @@ char *runtimeimport =
"func @\".efacethash (@\".i1·2 any) (@\".ret·1 uint32)\n"
"func @\".equal (@\".typ·2 *byte, @\".x1·3 any, @\".x2·4 any) (@\".ret·1 bool)\n"
"func @\".makemap (@\".mapType·2 *byte, @\".hint·3 int64) (@\".hmap·1 map[any]any)\n"
-"func @\".mapaccess1 (@\".mapType·2 *byte, @\".hmap·3 map[any]any, @\".key·4 any) (@\".val·1 any)\n"
+"func @\".mapaccess1 (@\".mapType·2 *byte, @\".hmap·3 map[any]any, @\".key·4 *any) (@\".val·1 *any)\n"
"func @\".mapaccess1_fast32 (@\".mapType·2 *byte, @\".hmap·3 map[any]any, @\".key·4 any) (@\".val·1 *any)\n"
"func @\".mapaccess1_fast64 (@\".mapType·2 *byte, @\".hmap·3 map[any]any, @\".key·4 any) (@\".val·1 *any)\n"
"func @\".mapaccess1_faststr (@\".mapType·2 *byte, @\".hmap·3 map[any]any, @\".key·4 any) (@\".val·1 *any)\n"
-"func @\".mapaccess2 (@\".mapType·3 *byte, @\".hmap·4 map[any]any, @\".key·5 any) (@\".val·1 any, @\".pres·2 bool)\n"
+"func @\".mapaccess2 (@\".mapType·3 *byte, @\".hmap·4 map[any]any, @\".key·5 *any) (@\".val·1 *any, @\".pres·2 bool)\n"
"func @\".mapaccess2_fast32 (@\".mapType·3 *byte, @\".hmap·4 map[any]any, @\".key·5 any) (@\".val·1 *any, @\".pres·2 bool)\n"
"func @\".mapaccess2_fast64 (@\".mapType·3 *byte, @\".hmap·4 map[any]any, @\".key·5 any) (@\".val·1 *any, @\".pres·2 bool)\n"
"func @\".mapaccess2_faststr (@\".mapType·3 *byte, @\".hmap·4 map[any]any, @\".key·5 any) (@\".val·1 *any, @\".pres·2 bool)\n"
-"func @\".mapassign1 (@\".mapType·1 *byte, @\".hmap·2 map[any]any, @\".key·3 any, @\".val·4 any)\n"
+"func @\".mapassign1 (@\".mapType·1 *byte, @\".hmap·2 map[any]any, @\".key·3 *any, @\".val·4 *any)\n"
"func @\".mapiterinit (@\".mapType·1 *byte, @\".hmap·2 map[any]any, @\".hiter·3 *any)\n"
-"func @\".mapdelete (@\".mapType·1 *byte, @\".hmap·2 map[any]any, @\".key·3 any)\n"
+"func @\".mapdelete (@\".mapType·1 *byte, @\".hmap·2 map[any]any, @\".key·3 *any)\n"
"func @\".mapiternext (@\".hiter·1 *any)\n"
-"func @\".mapiter1 (@\".hiter·2 *any) (@\".key·1 any)\n"
-"func @\".mapiter2 (@\".hiter·3 *any) (@\".key·1 any, @\".val·2 any)\n"
"func @\".makechan (@\".chanType·2 *byte, @\".hint·3 int64) (@\".hchan·1 chan any)\n"
"func @\".chanrecv1 (@\".chanType·2 *byte, @\".hchan·3 <-chan any) (@\".elem·1 any)\n"
"func @\".chanrecv2 (@\".chanType·3 *byte, @\".hchan·4 <-chan any) (@\".elem·1 any, @\".received·2 bool)\\n\"\n"
src/pkg/runtime/hashmap.c
マップアクセス関数の実装が変更され、キーが見つからない場合に t->elem->zero
を返すようになりました。また、mapaccess
や mapassign
の引数もポインタ型に変更されています。
--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -984,53 +980,26 @@ void
reflect·makemap(MapType *t, Hmap *ret)
{
ret = runtime·makemap_c(t, 0);
FLUSH(&ret);
}
-void
-runtime·mapaccess(MapType *t, Hmap *h, byte *ak, byte *av, bool *pres)
-{
- byte *res;
- Type *elem;
-
- elem = t->elem;
- if(h == nil || h->count == 0) {
- elem->alg->copy(elem->size, av, nil);
- *pres = false;
- return;
- }
-
- res = hash_lookup(t, h, &ak);
-
- if(res != nil) {
- *pres = true;
- elem->alg->copy(elem->size, av, res);
- } else {
- *pres = false;
- elem->alg->copy(elem->size, av, nil);
- }
-}
-
-// mapaccess1(hmap *map[any]any, key any) (val any);\n
+// mapaccess1(hmap *map[any]any, key *any) (val *any);\n
// NOTE: The returned pointer may keep the whole map live, so don\'t\n
// hold onto it for very long.\n #pragma textflag NOSPLIT\n void
-runtime·mapaccess1(MapType *t, Hmap *h, ...)\n
+runtime·mapaccess1(MapType *t, Hmap *h, byte *ak, byte *av)\n
{
- byte *ak, *av;\n- byte *res;\n-\n if(raceenabled && h != nil)\n runtime·racereadpc(h, runtime·getcallerpc(&t), runtime·mapaccess1);\n
- ak = (byte*)(&h + 1);\n- av = ak + ROUND(t->key->size, Structrnd);\n-\n if(h == nil || h->count == 0) {\n- t->elem->alg->copy(t->elem->size, av, nil);\n+ av = t->elem->zero;\n } else {\n- res = hash_lookup(t, h, &ak);\n- t->elem->alg->copy(t->elem->size, av, res);\n+ av = hash_lookup(t, h, &ak);\n+ if(av == nil)\n+ av = t->elem->zero;\n }\n
if(debug) {\n runtime·prints(\"runtime.mapaccess1: map=\");\n runtime·printpointer(h);\n@@ -1042,23 +1011,31 @@ runtime·mapaccess1(MapType *t, Hmap *h, ...)\n t->elem->alg->print(t->elem->size, av);\n runtime·prints(\"\\n\");\n }\n+ FLUSH(&av);\n }\n
-// mapaccess2(hmap *map[any]any, key any) (val any, pres bool);\n+// mapaccess2(hmap *map[any]any, key *any) (val *any, pres bool);\n // NOTE: The returned pointer keeps the whole map live, so don\'t\n // hold onto it for very long.\n #pragma textflag NOSPLIT\n void
-runtime·mapaccess2(MapType *t, Hmap *h, ...)\n
+runtime·mapaccess2(MapType *t, Hmap *h, byte *ak, byte *av, bool pres)\n {
- byte *ak, *av, *ap;\n-\n if(raceenabled && h != nil)\n runtime·racereadpc(h, runtime·getcallerpc(&t), runtime·mapaccess2);\n
- ak = (byte*)(&h + 1);\n- av = ak + ROUND(t->key->size, Structrnd);\n- ap = av + t->elem->size;\n-\n- runtime·mapaccess(t, h, ak, av, ap);\n+\tif(h == nil || h->count == 0) {\n+\t\tav = t->elem->zero;\n+\t\tpres = false;\n+\t} else {\n+\t\tav = hash_lookup(t, h, &ak);\n+\t\tif(av == nil) {\n+\t\t\tav = t->elem->zero;\n+\t\t\tpres = false;\n+\t\t} else {\n+\t\t\tpres = true;\n+\t\t}\n+\t}\n
if(debug) {\n runtime·prints(\"runtime.mapaccess2: map=\");\n runtime·printpointer(h);\n@@ -1068,9 +1045,11 @@ runtime·mapaccess2(MapType *t, Hmap *h, ...)\n runtime·prints(\"; val=\");\n t->elem->alg->print(t->elem->size, av);\n runtime·prints(\"; pres=\");\n- runtime·printbool(*ap);\n+ runtime·printbool(pres);\n runtime·prints(\"\\n\");\n }\n+ FLUSH(&av);\n+ FLUSH(&pres);\n }\n
// For reflect:\n@@ -1080,7 +1059,7 @@ runtime·mapaccess2(MapType *t, Hmap *h, ...)\n void\n reflect·mapaccess(MapType *t, Hmap *h, uintptr key, uintptr val, bool pres)\n {
- byte *ak, *av;\n+ byte *ak, *av, *r;\n
if(raceenabled && h != nil)\n runtime·racereadpc(h, runtime·getcallerpc(&t), reflect·mapaccess);\n
@@ -1089,77 +1068,63 @@ reflect·mapaccess(MapType *t, Hmap *h, uintptr key, uintptr val, bool pres)\n ak = (byte*)&key;\n else\n ak = (byte*)key;\n- val = 0;\n- pres = false;\n- if(t->elem->size <= sizeof(val))\n- av = (byte*)&val;\n- else {\n- av = runtime·mal(t->elem->size);\n- val = (uintptr)av;\n+
+ av = hash_lookup(t, h, &ak);\n+ if(av == nil) {\n+ val = 0;\n+ pres = false;\n+ } else {\n+ if(t->elem->size <= sizeof(val)) {\n+ val = 0; // clear high-order bits if value is smaller than a word\n+ t->elem->alg->copy(t->elem->size, &val, av);\n+ } else {\n+ // make a copy because reflect can hang on to result indefinitely\n+ r = runtime·cnew(t->elem);\n+ t->elem->alg->copy(t->elem->size, r, av);\n+ val = (uintptr)r;\n+ }\n+ pres = true;\n }\n- runtime·mapaccess(t, h, ak, av, &pres);\n FLUSH(&val);\n FLUSH(&pres);\n }
+// mapassign1(mapType *type, hmap *map[any]any, key *any, val *any);\n+#pragma textflag NOSPLIT\n void
-runtime·mapassign(MapType *t, Hmap *h, byte *ak, byte *av)\n+runtime·mapassign1(MapType *t, Hmap *h, byte *ak, byte *av)\n {
if(h == nil)\n runtime·panicstring(\"assignment to entry in nil map\");\n
- if(av == nil) {\n- hash_remove(t, h, ak);\n- } else {\n- hash_insert(t, h, ak, av);\n- }\n+ if(raceenabled)\n+ runtime·racewritepc(h, runtime·getcallerpc(&t), runtime·mapassign1);\n+
+ hash_insert(t, h, ak, av);\n
if(debug) {\n- runtime·prints(\"mapassign: map=\");\n+ runtime·prints(\"mapassign1: map=\");\n runtime·printpointer(h);\n runtime·prints(\"; key=\");\n t->key->alg->print(t->key->size, ak);\n runtime·prints(\"; val=\");\n- if(av)\n- t->elem->alg->print(t->elem->size, av);\n- else\n- runtime·prints(\"nil\");\n+ t->elem->alg->print(t->elem->size, av);\n runtime·prints(\"\\n\");\n }\n }
-// mapassign1(mapType *type, hmap *map[any]any, key any, val any);\n-#pragma textflag NOSPLIT\n-void\n-runtime·mapassign1(MapType *t, Hmap *h, ...)\n-{
- byte *ak, *av;\n-\n- if(h == nil)\n- runtime·panicstring(\"assignment to entry in nil map\");\n-\n- if(raceenabled)\n- runtime·racewritepc(h, runtime·getcallerpc(&t), runtime·mapassign1);\n- ak = (byte*)(&h + 1);\n- av = ak + ROUND(t->key->size, t->elem->align);\n-\n- runtime·mapassign(t, h, ak, av);\n-}
-
-// mapdelete(mapType *type, hmap *map[any]any, key any)\n+// mapdelete(mapType *type, hmap *map[any]any, key *any)\n #pragma textflag NOSPLIT\n void
-runtime·mapdelete(MapType *t, Hmap *h, ...)\n+runtime·mapdelete(MapType *t, Hmap *h, byte *ak)\n {
- byte *ak;\n-\n if(h == nil)\n return;\n
if(raceenabled)\n runtime·racewritepc(h, runtime·getcallerpc(&t), runtime·mapdelete);\n- ak = (byte*)(&h + 1);\n- runtime·mapassign(t, h, ak, nil);\n+
+ hash_remove(t, h, ak);\n
if(debug) {\n runtime·prints(\"mapdelete: map=\");\n runtime·printpointer(h);\n@@ -1187,13 +1152,35 @@ reflect·mapassign(MapType *t, Hmap *h, uintptr key, uintptr val, bool pres)\n ak = (byte*)&key;\n else\n ak = (byte*)key;\n- if(t->elem->size <= sizeof(val))\n- av = (byte*)&val;\n- else\n- av = (byte*)val;\n- if(!pres)\n- av = nil;\n- runtime·mapassign(t, h, ak, av);\n+ if(!pres) {\n+ hash_remove(t, h, ak);\n+
+ if(debug) {\n+ runtime·prints(\"mapassign: map=\");\n+ runtime·printpointer(h);\n+ runtime·prints(\"; key=\");\n+ t->key->alg->print(t->key->size, ak);\n+ runtime·prints(\"; val=nil\");\n+ runtime·prints(\"\\n\");\n+ }\n+ } else {\n+ if(t->elem->size <= sizeof(val))\n+ av = (byte*)&val;\n+ else\n+ av = (byte*)val;\n+
+ hash_insert(t, h, ak, av);\n+
+ if(debug) {\n+ runtime·prints(\"mapassign: map=\");\n+ runtime·printpointer(h);\n+ runtime·prints(\"; key=\");\n+ t->key->alg->print(t->key->size, ak);\n+ runtime·prints(\"; val=\");\n+ t->elem->alg->print(t->elem->size, av);\n+ runtime·prints(\"\\n\");\n+ }\n+ }\n }\n
// mapiterinit(mapType *type, hmap *map[any]any, hiter *any);\n@@ -1254,46 +1241,6 @@ reflect·mapiternext(struct hash_iter *it)\n runtime·mapiternext(it);\n }\n
-// mapiter1(hiter *any) (key any);\n-#pragma textflag NOSPLIT\n-void\n-runtime·mapiter1(struct hash_iter *it, ...)\n-{\n- byte *ak, *res;\n- Type *key;\n-\n- ak = (byte*)(&it + 1);\n-\n- res = it->key;\n- if(res == nil)\n- runtime·throw(\"runtime.mapiter1: key:val nil pointer\");\n-\n- key = it->t->key;\n- key->alg->copy(key->size, ak, res);\n-\n- if(debug) {\n- runtime·prints(\"mapiter1: iter=\");\n- runtime·printpointer(it);\n- runtime·prints(\"; map=\");\n- runtime·printpointer(it->h);\n- runtime·prints(\"\\n\");\n- }\n-}
-
-bool
-runtime·mapiterkey(struct hash_iter *it, void *ak)\n-{
- byte *res;\n- Type *key;\n-\n- res = it->key;\n- if(res == nil)\n- return false;\n- key = it->t->key;\n- key->alg->copy(key->size, ak, res);\n- return true;\n-}
-
// For reflect:\n // func mapiterkey(h map) (key iword, ok bool)\n // where an iword is the same word an interface value would use:\n@@ -1301,18 +1248,24 @@ runtime·mapiterkey(struct hash_iter *it, void *ak)\n void\n reflect·mapiterkey(struct hash_iter *it, uintptr key, bool ok)\n {
- byte *res;\n+ byte *res, *r;\n Type *tkey;\n
- key = 0;\n- ok = false;\n res = it->key;\n- if(res != nil) {\n+ if(res == nil) {\n+ key = 0;\n+ ok = false;\n+ } else {\n tkey = it->t->key;\n- if(tkey->size <= sizeof(key))\n+ if(tkey->size <= sizeof(key)) {\n+ key = 0; // clear high-order bits if value is smaller than a word\n tkey->alg->copy(tkey->size, (byte*)&key, res);\n- else\n- key = (uintptr)res;\n+ } else {\n+ // make a copy because reflect can hang on to result indefinitely\n+ r = runtime·cnew(tkey);\n+ tkey->alg->copy(tkey->size, r, res);\n+ key = (uintptr)r;\n+ }\n ok = true;\n }\n FLUSH(&key);\n@@ -1335,33 +1288,5 @@ reflect·maplen(Hmap *h, intgo len)\n FLUSH(&len);\n }\n
-// mapiter2(hiter *any) (key any, val any);\n-#pragma textflag NOSPLIT\n-void\n-runtime·mapiter2(struct hash_iter *it, ...)\n-{\n- byte *ak, *av, *res;\n- tMapType *t;\n-\n- t = it->t;\n- ak = (byte*)(&it + 1);\n- av = ak + ROUND(t->key->size, t->elem->align);\n-\n- res = it->key;\n- if(res == nil)\n- runtime·throw(\"runtime.mapiter2: key:val nil pointer\");\n-\n- t->key->alg->copy(t->key->size, ak, res);\n- t->elem->alg->copy(t->elem->size, av, it->value);\n-\n- if(debug) {\n- runtime·prints(\"mapiter2: iter=\");\n- runtime·printpointer(it);\n- runtime·prints(\"; map=\");\n- runtime·printpointer(it->h);\n- runtime·prints(\"\\n\");\n- }\n-}
-
// exported value for testing\n float64 runtime·hashLoad = LOAD;\n```
### `src/pkg/reflect/type.go`
`rtype` 構造体に `zero` フィールドが追加され、型のゼロ値へのポインタを保持するようになりました。
```diff
--- a/src/pkg/reflect/type.go
+++ b/src/pkg/reflect/type.go
@@ -252,6 +252,7 @@ type rtype struct {
string *string // string form; unnecessary but undeniably useful
*uncommonType // (relatively) uncommon fields
ptrToThis *rtype // type for pointer to this type, if used in binary or has methods
+ zero unsafe.Pointer // pointer to zero value
}
// Method on non-interface type
@@ -1089,6 +1090,7 @@ func (t *rtype) ptrTo() *rtype {
p.uncommonType = nil
p.ptrToThis = nil
+ p.zero = unsafe.Pointer(&make([]byte, p.size)[0])
p.elem = t
if t.kind&kindNoPointers != 0 {
@@ -1475,6 +1477,7 @@ func ChanOf(dir ChanDir, t Type) Type {
ch.elem = typ
ch.uncommonType = nil
ch.ptrToThis = nil
+ ch.zero = unsafe.Pointer(&make([]byte, ch.size)[0])
ch.gc = unsafe.Pointer(&chanGC{
width: ch.size,
@@ -1534,6 +1537,7 @@ func MapOf(key, elem Type) Type {
mt.hmap = hMapOf(mt.bucket)
mt.uncommonType = nil
mt.ptrToThis = nil
+ mt.zero = unsafe.Pointer(&make([]byte, mt.size)[0])
// INCORRECT. Uncomment to check that TestMapOfGC and TestMapOfGCValues
// fail when mt.gc is wrong.
@@ -1709,6 +1713,7 @@ func SliceOf(t Type) Type {
slice.elem = typ
slice.uncommonType = nil
slice.ptrToThis = nil
+ slice.zero = unsafe.Pointer(&make([]byte, slice.size)[0])
if typ.size == 0 {
slice.gc = unsafe.Pointer(&sliceEmptyGCProg)
@@ -1778,6 +1783,7 @@ func arrayOf(count int, elem Type) Type {
// TODO: array.gc
array.uncommonType = nil
array.ptrToThis = nil
+ array.zero = unsafe.Pointer(&make([]byte, array.size)[0])
array.len = uintptr(count)
array.slice = slice.(*rtype)
コアとなるコードの解説
マップアクセサの引数変更 (src/cmd/gc/builtin.c
, src/pkg/runtime/hashmap.c
)
src/cmd/gc/builtin.c
では、Goコンパイラが内部的に使用するランタイム関数のシグネチャが定義されています。このコミットでは、mapaccess1
, mapaccess2
, mapassign1
, mapdelete
といったマップ操作関数のキーと値の引数が、any
(値渡し) から *any
(ポインタ渡し) に変更されました。
例えば、mapaccess1
は以前 (mapType *byte, hmap map[any]any, key any) (val any)
でしたが、変更後は (mapType *byte, hmap map[any]any, key *any) (val *any)
となっています。
これは、src/pkg/runtime/hashmap.c
にある実際のランタイム関数の実装と同期しています。以前は、これらの関数は可変長引数(...
)を使用してスタックから直接引数を読み取っていましたが、この変更により、明示的にポインタを受け取るようになりました。これにより、GCがスタックをスキャンする際に、ポインタの位置と型を正確に識別できるようになり、GCの精度が向上します。また、大きなキーや値のコピーが不要になるため、パフォーマンスも改善されます。
ゼロ値メモリの導入 (src/pkg/reflect/type.go
, src/pkg/runtime/hashmap.c
, src/cmd/gc/reflect.c
)
src/pkg/reflect/type.go
の rtype
構造体に zero unsafe.Pointer
フィールドが追加されました。このフィールドは、その型に対応するゼロ値が格納されたメモリ領域へのポインタを保持します。
src/pkg/runtime/hashmap.c
の runtime·mapaccess1
や runtime·mapaccess2
関数では、マップからキーが見つからなかった場合に、以前はゼロ値をコピーして返していましたが、変更後は t->elem->zero
(要素型のゼロ値へのポインタ) を返すようになりました。
src/cmd/gc/reflect.c
の dcommontype
関数では、各型に対応する zerovalue
シンボルが定義され、リンカがこのゼロ値メモリ領域を確保するよう指示しています。この zerovalue
は、すべてのマップ型で共有される単一のメモリ領域であり、メモリ効率を高めます。
この変更により、マップアクセスでキーが見つからない場合の処理が簡素化され、ランタイムのコードがよりクリーンになります。また、リフレクションAPIの Zero
関数もこの t.zero
フィールドを利用するようになり、ゼロ値の取得が一貫した方法で行われるようになりました。
コンパイラのコード生成変更 (src/cmd/gc/walk.c
)
src/cmd/gc/walk.c
はGoコンパイラの重要な部分で、Goのソースコードから中間表現(AST)を変換し、最終的な機械語コードを生成する前の最適化やコード生成の準備を行います。
このコミットでは、マップ操作(アクセス、代入、削除)に関するコード生成ロジックが大幅に変更されました。具体的には、マップ操作のキーや値がリテラルや一時的な値である場合、コンパイラはそれらを一時変数に格納し、その一時変数のアドレス(ポインタ)をランタイム関数に渡すようにコードを変換します。
例えば、a, b = m[i]
のようなマップアクセスの場合、以前は mapaccess2
が直接 i
の値を引数として受け取っていましたが、変更後は i
が一時変数に格納され、その一時変数へのポインタが mapaccess2
に渡されるようになります。これにより、ランタイム関数が常にポインタを受け取るという新しい規約が守られます。
また、for range
ループにおけるマップイテレーションの内部処理も変更され、mapiter1
や mapiter2
といったランタイム関数が削除されました。代わりに、イテレータ構造体 hash_iter
から直接キーと値のポインタを取得するようになり、不要な関数呼び出しとコピーが削減されています。
これらの変更は、Goコンパイラが生成するコードが、新しい参照渡し規約に準拠し、より効率的になることを保証します。
関連リンク
- Go Change-Id:
I3278dc158e34779eb46cd1b5a73c1d0c18602184
(Gerrit Code Review) - Go CL 14794043: https://golang.org/cl/14794043
- GitHub Commit: https://github.com/golang/go/commit/3278dc158e34779eb46cd1b5a73c1d0c18602184
参考にした情報源リンク
- Goのマップ実装に関する一般的な情報:
- Goのガベージコレクションとスタックスキャンに関する情報:
- Goの内部的な型表現とリフレクションに関する情報:
- C言語の可変長引数に関する一般的な情報:
- Goのゼロ値に関する情報:
- Goのコンパイラとランタイムのソースコード (直接参照)
src/cmd/gc/
src/pkg/runtime/
src/pkg/reflect/