[インデックス 16395] ファイルの概要
このコミットは、Go言語のランタイムにおけるマップ(map
)のイテレーション(range
ループ)に関するパフォーマンス改善を目的としています。具体的には、空のマップをイテレートする際の処理を高速化しています。
変更されたファイルは以下の2つです。
src/pkg/runtime/hashmap.c
: Goランタイムにおけるハッシュマップの実装に関連するC言語のソースファイルです。マップの初期化やイテレーションのロジックが含まれています。このコミットでは、マップイテレータの初期化関数に条件分岐が追加されています。src/pkg/runtime/mapspeed_test.go
: Goランタイムのマップのパフォーマンスを測定するためのベンチマークテストファイルです。このコミットでは、マップのイテレーションに関する新しいベンチマークテストが追加されています。
コミット
このコミットは、Goランタイムにおいて空のマップに対するrange
ループのパフォーマンスを向上させます。ベンチマーク結果によると、空のマップのイテレーションが約79%高速化されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7b9df092618d593e29ff8e7a509a5ad110431439
元コミット内容
commit 7b9df092618d593e29ff8e7a509a5ad110431439
Author: Frederick Kelly Mayle III <frederickmayle@gmail.com>
Date: Thu May 23 14:17:52 2013 -0700
runtime: faster range on empty map
benchmark old ns/op new ns/op delta
BenchmarkMapIter 191 190 -0.52%
BenchmarkMapIterEmpty 22 4 -78.96%
R=golang-dev, minux.ma, dvyukov, iant, khr
CC=golang-dev
https://golang.org/cl/9637043
変更の背景
Go言語では、map
型は非常に頻繁に使用されるデータ構造です。for range
ループを使ってマップをイテレートする機能は、Goの強力な特徴の一つです。しかし、マップが空である場合でも、イテレーションの準備のために一定のオーバーヘッドが発生していました。
このコミットの背景には、空のマップに対するrange
ループが、実際には何も処理しないにもかかわらず、不必要な初期化処理を実行しているという認識がありました。特に、空のマップを頻繁にイテレートするようなシナリオ(例えば、エラーハンドリングで空のマップが返される場合や、初期状態のデータ構造を扱う場合など)では、このオーバーヘッドが累積してパフォーマンスに影響を与える可能性がありました。
ベンチマーク結果が示すように、BenchmarkMapIterEmpty
の実行時間が22nsから4nsへと大幅に短縮されており、これは空のマップのイテレーションが約79%高速化されたことを意味します。この改善は、Goアプリケーション全体のパフォーマンス、特にマップの利用が多いコードベースにおいて、わずかながらも積み重なる形で貢献します。
前提知識の解説
Go言語のmap
型
Go言語のmap
は、キーと値のペアを格納するハッシュテーブルの実装です。キーは一意であり、値は任意の型を持つことができます。map
は参照型であり、make
関数で初期化されます。
m := make(map[string]int) // stringキーとint値を持つマップを作成
m["apple"] = 1
v := m["apple"] // 値の取得
delete(m, "apple") // 要素の削除
for range
ループとマップのイテレーション
Go言語では、for range
ループを使用してマップのキーと値をイテレートできます。
for key, value := range myMap {
// keyとvalueを使った処理
}
マップのイテレーション順序は保証されません。
Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、プリミティブ型の操作、システムコールなど、Goプログラムが動作するために必要な低レベルの機能を提供します。src/pkg/runtime
ディレクトリには、これらのランタイム機能のソースコードが含まれています。Goランタイムの多くの部分はC言語(またはGoのサブセットであるPlan 9 C)で書かれていますが、一部はGo言語自体で書かれています。
hashmap.c
src/pkg/runtime/hashmap.c
は、Goランタイムにおけるハッシュマップ(map
)の内部実装の一部を担うC言語のファイルです。マップの作成、要素の追加・削除、イテレーションの準備など、マップ操作の低レベルなロジックが含まれています。Goのmap
操作は、最終的にこのC言語のコードにコンパイルされて実行されます。
mapspeed_test.go
src/pkg/runtime/mapspeed_test.go
は、Goランタイムのマップ操作のパフォーマンスを測定するためのベンチマークテストが記述されたGo言語のファイルです。Goのtesting
パッケージを利用して、特定の操作(マップの作成、要素の追加、イテレーションなど)にかかる時間を計測し、パフォーマンスの回帰を検出したり、改善の効果を評価したりするために使用されます。
技術的詳細
このコミットの技術的な核心は、Goランタイムのマップイテレータ初期化関数runtime·mapiterinit
における条件分岐の追加です。
Go言語でfor range
ループを使ってマップをイテレートする際、内部的にはランタイムのruntime·mapiterinit
関数が呼び出され、イテレーションに必要な準備が行われます。この関数は、イテレータの状態を初期化し、マップの要素を走査するための準備をします。
変更前は、runtime·mapiterinit
関数は、マップのポインタh
がnil
(マップが初期化されていない状態)の場合にのみ早期リターンしていました。しかし、マップがnil
ではないが、count
(要素数)が0である「空のマップ」の場合には、不要な初期化処理が実行されていました。
このコミットでは、以下の条件が追加されました。
- if(h == nil) {
+ if(h == nil || h->count == 0) {
この変更により、h
がnil
であるか、またはh->count
が0
である(つまり、マップが空である)場合に、runtime·mapiterinit
関数はすぐにit->key = nil; return;
を実行して処理を終了するようになりました。
h == nil
: マップ変数がnil
である場合(例:var m map[string]int
の初期状態)。h->count == 0
: マップが初期化されているが、要素が一つも追加されていない場合(例:m := make(map[string]int)
の直後)。
この最適化により、空のマップに対してrange
ループが実行された際に、イテレータの複雑な初期化ロジックや、ハッシュテーブルの内部構造を走査しようとする試みが完全にスキップされるようになります。これにより、CPUサイクルとメモリアクセスの両方が削減され、特に空のマップのイテレーションが頻繁に行われる場合に顕著なパフォーマンス向上をもたらします。
ベンチマーク結果のBenchmarkMapIterEmpty
が示すように、この変更によって空のマップのイテレーションにかかる時間が大幅に短縮されました。一方、要素を持つマップのイテレーション(BenchmarkMapIter
)にはほとんど影響がないことも確認されており、これは既存の動作に悪影響を与えずに特定のケースを最適化したことを示しています。
コアとなるコードの変更箇所
src/pkg/runtime/hashmap.c
の runtime·mapiterinit
関数内の変更点:
diff --git a/src/pkg/runtime/hashmap.c b/src/pkg/runtime/hashmap.c
index 892f0a1700..959d6bc760 100644
--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -1355,7 +1355,7 @@ reflect·mapassign(MapType *t, Hmap *h, uintptr key, uintptr val, bool pres)\n void\n runtime·mapiterinit(MapType *t, Hmap *h, struct hash_iter *it)\n {\n-\tif(h == nil) {\n+\tif(h == nil || h->count == 0) {\n \t\tit->key = nil;\n \t\treturn;\n \t}\
src/pkg/runtime/mapspeed_test.go
に追加されたベンチマークテスト:
diff --git a/src/pkg/runtime/mapspeed_test.go b/src/pkg/runtime/mapspeed_test.go
index a737c65dc6..13b57621d4 100644
--- a/src/pkg/runtime/mapspeed_test.go
+++ b/src/pkg/runtime/mapspeed_test.go
@@ -233,3 +233,24 @@ func BenchmarkNewEmptyMap(b *testing.B) {\n \t_ = make(map[int]int)\n \t}\n }\n+\n+func BenchmarkMapIter(b *testing.B) {\n+\tm := make(map[int]bool)\n+\tfor i := 0; i < 8; i++ {\n+\t\tm[i] = true\n+\t}\n+\tb.ResetTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tfor _, _ = range m {\n+\t\t}\n+\t}\n+}\n+\n+func BenchmarkMapIterEmpty(b *testing.B) {\n+\tm := make(map[int]bool)\n+\tb.ResetTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tfor _, _ = range m {\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/runtime/hashmap.c` の変更
`runtime·mapiterinit` 関数は、Goの`map`に対する`for range`ループが開始される際に、ランタイムによって呼び出される内部関数です。この関数の目的は、マップのイテレーションに必要な内部状態(イテレータ構造体`it`)を初期化することです。
変更前のコード:
```c
if(h == nil) {
it->key = nil;
return;
}
これは、マップのポインタh
がnil
(Goのnil
マップ)の場合に、イテレータのキーをnil
に設定してすぐにリターンするというものでした。これは正しい動作ですが、make(map[K]V)
で作成されたが要素が一つも追加されていない「空のマップ」の場合には、この条件に合致せず、イテレータの初期化処理が続行されていました。
変更後のコード:
if(h == nil || h->count == 0) {
it->key = nil;
return;
}
この変更では、既存のh == nil
の条件に加えて、h->count == 0
という条件が追加されました。
h
:Hmap
構造体へのポインタで、Goのマップの内部表現です。h->count
:Hmap
構造体内のフィールドで、マップに現在格納されている要素の数を表します。
この新しい条件h->count == 0
が追加されたことで、マップがnil
であるか、またはマップに要素が一つも含まれていない(空である)場合に、イテレータの初期化処理が早期に終了するようになりました。これにより、空のマップに対するrange
ループが、不必要な内部処理(例えば、バケットの走査やハッシュ計算の準備など)を実行することなく、即座に終了するようになり、パフォーマンスが向上します。
src/pkg/runtime/mapspeed_test.go
の変更
このファイルには、マップのイテレーションのパフォーマンスを測定するための2つの新しいベンチマーク関数が追加されました。
-
BenchmarkMapIter
:func BenchmarkMapIter(b *testing.B) { m := make(map[int]bool) for i := 0; i < 8; i++ { m[i] = true } b.ResetTimer() for i := 0; i < b.N; i++ { for _, _ = range m { } } }
このベンチマークは、8つの要素を持つマップをイテレートする際のパフォーマンスを測定します。これは、一般的なマップイテレーションのシナリオをシミュレートし、最適化が既存の動作に悪影響を与えないことを確認するために使用されます。
-
BenchmarkMapIterEmpty
:func BenchmarkMapIterEmpty(b *testing.B) { m := make(map[int]bool) b.ResetTimer() for i := 0; i < b.N; i++ { for _, _ = range m { } } }
このベンチマークは、空のマップをイテレートする際のパフォーマンスを測定します。このベンチマークが、今回のコミットによるパフォーマンス改善の主要なターゲットであり、その効果を定量的に示すために使用されます。
これらのベンチマークは、Goのtesting
パッケージのb.ResetTimer()
とb.N
を使用して、指定された操作をb.N
回繰り返し実行し、その平均実行時間をナノ秒単位で測定します。コミットメッセージに記載されているベンチマーク結果は、これらのテストが実際に実行され、改善が確認されたことを示しています。
関連リンク
- Go CL 9637043: https://golang.org/cl/9637043
参考にした情報源リンク
- なし (提供されたコミット情報とGo言語の一般的な知識に基づいて解説を生成しました。)