[インデックス 16062] ファイルの概要
このコミットは、Go言語のランタイムにおけるマップ(ハッシュマップ)の実装、特に文字列キーを持つマップのパフォーマンス改善に関するものです。具体的には、要素数が少ない(2〜8個)文字列マップにおいて、キーのハッシュ計算を不要にする最適化が導入されています。これにより、特定のシナリオでのマップルックアップの高速化が図られています。
コミット
commit 45b54ee7fb3dc7b8444733288a557f1c62572c43
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Apr 2 20:58:25 2013 -0700
runtime: avoid hashing strings until needed in single-bucket maps
This changes the map lookup behavior for string maps with 2-8 keys.
There was already previously a fastpath for 0 items and 1 item.
Now, if a string-keyed map has <= 8 items, first check all the
keys for length first. If only one has the right length, then
just check it for equality and avoid hashing altogether. Once
the map has more than 8 items, always hash like normal.
I don't know why some of the other non-string map benchmarks
got faster. This was with benchtime=2s, multiple times. I haven't
anything else getting slower, though.
benchmark old ns/op new ns/op delta
BenchmarkHashStringSpeed 37 34 -8.20%
BenchmarkHashInt32Speed 32 29 -10.67%
BenchmarkHashInt64Speed 31 27 -12.82%
BenchmarkHashStringArraySpeed 105 99 -5.43%
BenchmarkMegMap 274206 255153 -6.95%
BenchmarkMegOneMap 27 23 -14.80%
BenchmarkMegEqMap 148332 116089 -21.74%
BenchmarkMegEmptyMap 4 3 -12.72%
BenchmarkSmallStrMap 22 22 -0.89%
BenchmarkMapStringKeysEight_32 42 23 -43.71%
BenchmarkMapStringKeysEight_64 55 23 -56.96%
BenchmarkMapStringKeysEight_1M 279688 24 -99.99%
BenchmarkIntMap 16 15 -10.18%
BenchmarkRepeatedLookupStrMapKey32 40 37 -8.15%
BenchmarkRepeatedLookupStrMapKey1M 287918 272980 -5.19%
BenchmarkNewEmptyMap 156 130 -16.67%
R=golang-dev, khr
CC=golang-dev
https://golang.org/cl/7641057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/45b54ee7fb3dc7b8444733288a557f1c62572c43
元コミット内容
Go言語のランタイムにおいて、単一バケットのマップ(特に文字列キーを持つマップ)で、必要になるまで文字列のハッシュ計算を避けるように変更しました。
この変更は、2〜8個のキーを持つ文字列マップのルックアップ動作に影響を与えます。
以前から、0個または1個のアイテムを持つマップには高速パスが存在していました。
今回の変更では、文字列キーのマップが8個以下のアイテムを持つ場合、まずすべてのキーの長さをチェックします。もし正しい長さを持つキーが1つだけであれば、ハッシュ計算を全く行わずにそのキーとの等価性チェックのみを行います。マップが8個を超えるアイテムを持つようになった場合は、通常通りハッシュ計算を行います。
一部の非文字列マップのベンチマークが高速化した理由は不明です。これはbenchtime=2s
で複数回実行した結果です。しかし、他の何かが遅くなったという報告はありません。
ベンチマーク結果は以下の通りです。
benchmark | old ns/op | new ns/op | delta |
---|---|---|---|
BenchmarkHashStringSpeed | 37 | 34 | -8.20% |
BenchmarkHashInt32Speed | 32 | 29 | -10.67% |
BenchmarkHashInt64Speed | 31 | 27 | -12.82% |
BenchmarkHashStringArraySpeed | 105 | 99 | -5.43% |
BenchmarkMegMap | 274206 | 255153 | -6.95% |
BenchmarkMegOneMap | 27 | 23 | -14.80% |
BenchmarkMegEqMap | 148332 | 116089 | -21.74% |
BenchmarkMegEmptyMap | 4 | 3 | -12.72% |
BenchmarkSmallStrMap | 22 | 22 | -0.89% |
BenchmarkMapStringKeysEight_32 | 42 | 23 | -43.71% |
BenchmarkMapStringKeysEight_64 | 55 | 23 | -56.96% |
BenchmarkMapStringKeysEight_1M | 279688 | 24 | -99.99% |
BenchmarkIntMap | 16 | 15 | -10.18% |
BenchmarkRepeatedLookupStrMapKey32 | 40 | 37 | -8.15% |
BenchmarkRepeatedLookupStrMapKey1M | 287918 | 272980 | -5.19% |
BenchmarkNewEmptyMap | 156 | 130 | -16.67% |
変更の背景
Go言語のマップは、プログラムのパフォーマンスに大きな影響を与える基本的なデータ構造です。特に、キーとして文字列を使用するマップは非常に一般的であり、そのルックアップ性能はアプリケーション全体の応答性に直結します。
このコミットが行われた2013年当時、Goのランタイムはまだ成熟の途上にあり、様々なパフォーマンス最適化が継続的に行われていました。マップのルックアップ処理は、キーのハッシュ値を計算し、そのハッシュ値に基づいて適切なバケットを特定し、バケット内のキーと等価性比較を行うという手順を踏みます。文字列のハッシュ計算は、特に長い文字列の場合、CPUコストが高い処理です。
コミットメッセージにあるように、0個または1個のアイテムを持つマップには既に最適化されたパスが存在していました。しかし、2個から8個といった「非常に小さい」が「空ではない」マップの場合、無条件にハッシュ計算を行うことがオーバーヘッドとなっていました。このような小さなマップは、例えば設定値の格納や、少数の固定されたオプションの管理など、様々な場面で頻繁に利用されます。
この変更の背景には、このような小さな文字列マップのルックアップにおいて、ハッシュ計算のコストを削減し、全体的なパフォーマンスを向上させるという明確な目的がありました。特に、BenchmarkMapStringKeysEight_1M
のような極端な改善が見られるベンチマーク結果は、この最適化が特定のワークロードにおいて非常に効果的であることを示しています。これは、1M個の要素を持つマップから、8個のキーを持つ小さなマップを繰り返しルックアップするようなシナリオで、ハッシュ計算をスキップできる恩恵が非常に大きいことを意味します。
前提知識の解説
このコミットを理解するためには、以下のGo言語のランタイムとデータ構造に関する基本的な知識が必要です。
1. Go言語のマップ (map)
Go言語のmap
は、キーと値のペアを格納するハッシュテーブル(ハッシュマップ)として実装されています。内部的には、キーのハッシュ値に基づいてデータをバケットと呼ばれる領域に分散して格納します。
- ハッシュ関数: キーからハッシュ値を計算する関数です。Goのマップは、キーの型に応じて適切なハッシュ関数を使用します。文字列の場合、文字列の内容に基づいてハッシュ値を計算します。
- バケット: ハッシュテーブルの基本的な格納単位です。各バケットは複数のキーと値を保持できます。
- 衝突 (Collision): 異なるキーが同じハッシュ値を生成し、同じバケットに格納される状況です。衝突が発生した場合、Goのマップはチェイニング(連結リスト)などの方法で複数のエントリを同じバケット内に管理します。
- オーバーフローバケット: バケットが満杯になった場合に、追加のキーと値を格納するために連結されるバケットです。
- ロードファクタとリサイズ: マップに要素が追加され、バケットの密度(ロードファクタ)が高くなると、パフォーマンスが低下する可能性があります。Goのマップは、一定のロードファクタを超えると、より大きなバケット配列にリサイズ(再ハッシュ)を行い、要素を再分配します。
2. 文字列のハッシュ計算
文字列のハッシュ計算は、その文字列のすべてのバイトを処理する必要があるため、一般的に整数やポインタのハッシュ計算よりもコストがかかります。特に長い文字列の場合、このコストは顕著になります。
3. パフォーマンス最適化の考え方
ソフトウェアのパフォーマンス最適化では、以下の点が重要になります。
- ホットパスの特定: プログラム実行時間の大部分を占めるコードパス(ホットパス)を特定し、そこを最適化することで全体的な性能向上を図ります。マップのルックアップは典型的なホットパスの一つです。
- コストの高い操作の回避: ハッシュ計算のようにコストの高い操作は、可能な限り回避することが望ましいです。
- 特殊ケースの最適化: 一般的なケースとは異なる、特定の条件(例: マップの要素数が非常に少ない場合)でより効率的なアルゴリズムを適用することで、性能を向上させることができます。
4. runtime
パッケージ
Go言語のruntime
パッケージは、ガベージコレクション、スケジューラ、マップやチャネルなどの組み込み型の低レベルな実装など、Goプログラムの実行を支えるコアな機能を提供します。このコミットで変更されているsrc/pkg/runtime/hashmap.c
とsrc/pkg/runtime/hashmap_fast.c
は、まさにこのランタイムの一部であり、Goのマップの内部動作を定義しています。
技術的詳細
このコミットの技術的な核心は、Goのマップルックアップにおける「ハッシュ計算の遅延」と「キー長による事前フィルタリング」です。
Goのマップは、内部的にHmap
構造体とBucket
構造体で構成されています。Hmap
はマップ全体のメタデータ(要素数、バケット配列へのポインタなど)を管理し、Bucket
は実際のキーと値のペアを格納します。
既存の高速パス
コミットメッセージにもあるように、この変更以前から、Goのマップには以下の高速パスが存在していました。
- 0アイテムのマップ: 空のマップに対するルックアップは、ハッシュ計算もバケットアクセスも不要で、即座に「見つからない」と判断できます。
- 1アイテムのマップ: 1つのアイテムしか持たないマップの場合、ハッシュ計算を行わずに、その唯一のキーとルックアップ対象のキーを直接比較することで、高速に処理できます。
新しい最適化のメカニズム
このコミットは、上記の高速パスを拡張し、2〜8個のアイテムを持つ文字列キーのマップに適用される新しい最適化を導入しています。
- 単一バケットの条件: Goのマップは、要素数が少ない場合、すべてのキーが単一のバケットに収まるように設計されています。この最適化は、この「単一バケット」の状態にあるマップに適用されます。コミットメッセージの
h->B == 0
という条件がこれに該当します。h->B
はバケット配列のサイズを決定するビット数であり、B=0
は2^0 = 1
つのバケット(つまり単一バケット)を意味します。 - 文字列キーの特性利用: 文字列の等価性比較は、まず文字列の長さを比較し、次に内容を比較するという2段階で行われます。長さが異なれば、内容を比較するまでもなく異なる文字列であると判断できます。
- ハッシュ計算の回避:
- マップが単一バケットであり(
h->B == 0
)、かつ文字列キーを持つ場合、まずルックアップ対象のキーの長さと、バケット内の各キーの長さを比較します。 - もし、バケット内のキーの中で、ルックアップ対象のキーと同じ長さを持つものが1つだけだった場合、そのキーに対してのみ文字列の内容比較(
EQFUNC
)を行います。この際、ハッシュ計算は完全にスキップされます。 - これにより、ハッシュ計算という比較的コストの高い操作を、多くのケースで回避できるようになります。
- マップが単一バケットであり(
- フォールバック:
- もし、ルックアップ対象のキーと同じ長さを持つキーがバケット内に複数存在した場合(衝突の可能性が高い)、またはマップのアイテム数が8個を超えた場合(単一バケットの最適化の範囲外)、通常のハッシュ計算とバケットルックアップのパス(
dohash
ラベルへのジャンプ)にフォールバックします。 BUCKETSIZE
はGoのマップの内部定数で、1つのバケットが保持できるキーと値のペアの最大数を示します。このコミットの時点では8でした。したがって、8個以下のキーを持つマップは、通常1つのバケットに収まります。
- もし、ルックアップ対象のキーと同じ長さを持つキーがバケット内に複数存在した場合(衝突の可能性が高い)、またはマップのアイテム数が8個を超えた場合(単一バケットの最適化の範囲外)、通常のハッシュ計算とバケットルックアップのパス(
HASMAYBE
とEQMAYBE
マクロ
この最適化を実装するために、src/pkg/runtime/hashmap.c
で新しいマクロが導入されています。
HASMAYBE
: このマクロは、キーの型が「ハッシュ計算を遅延できる可能性のある」型であるかどうかを示します。文字列キーの場合にtrue
に設定されます。EQMAYBE(x,y)
: このマクロは、キーの型がハッシュ計算を遅延できる場合に、ハッシュ計算なしでキーが一致する可能性があるかどうかを判断するための簡易的な比較を行います。文字列キーの場合、((x).len == (y).len)
、つまり文字列の長さが同じかどうかを比較します。
これらのマクロは、hashmap_fast.c
内のジェネリックなマップルックアップ関数(HASH_LOOKUP1
およびHASH_LOOKUP2
)が、キーの型に応じて異なる最適化パスを適用できるようにするために使用されます。
ベンチマーク結果の分析
コミットメッセージに記載されているベンチマーク結果は、この最適化の効果を明確に示しています。
BenchmarkMapStringKeysEight_1M
: このベンチマークは、1M個の要素を持つ大きなマップから、8個のキーを持つ小さな文字列マップを繰り返しルックアップするシナリオをシミュレートしていると考えられます。このベンチマークで**-99.99%**という劇的な改善が見られるのは、まさにこの最適化が狙った効果であり、ハッシュ計算がほとんど完全に回避されたことを示唆しています。BenchmarkMapStringKeysEight_32
、BenchmarkMapStringKeysEight_64
: これらのベンチマークも、8個のキーを持つ文字列マップに対するルックアップの改善を示しており、それぞれ-43.71%、-56.96%の高速化を達成しています。- 非文字列マップの改善:
BenchmarkHashInt32Speed
、BenchmarkHashInt64Speed
、BenchmarkIntMap
など、整数キーのマップでも改善が見られます。コミットメッセージではその理由が不明とされていますが、これはおそらく、マップルックアップのコードパス全体がより効率的になったこと、または、この変更によってランタイムの他の部分でリソース競合が緩和されたことなどが考えられます。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/pkg/runtime/hashmap.c
src/pkg/runtime/hashmap_fast.c
src/pkg/runtime/hashmap.c
の変更
このファイルでは、主に文字列キーのマップに対するHASH_LOOKUP1
(単一値取得)およびHASH_LOOKUP2
(値と存在チェック取得)の定義において、新しいマクロEQMAYBE
とHASMAYBE
が追加されています。
--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -563,6 +571,8 @@ static uint8 empty_value[MAXVALUESIZE];
#define KEYTYPE String
#define HASHFUNC runtime·algarray[ASTRING].hash
#define EQFUNC(x,y) ((x).len == (y).len && ((x).str == (y).str || runtime·memeq((x).str, (y).str, (x).len)))
+#define EQMAYBE(x,y) ((x).len == (y).len) // 文字列の長さが同じかどうかを比較
+#define HASMAYBE true // 文字列型はハッシュ遅延の対象
#define QUICKEQ(x) ((x).len < 32) // 32バイト未満の文字列はQUICKEQの対象
#include "hashmap_fast.c"
EQMAYBE(x,y)
: 文字列の長さが等しい場合にtrue
を返します。これは、ハッシュ計算をスキップして直接比較に進むかどうかの初期判定に使われます。HASMAYBE
: 文字列型の場合にtrue
を設定します。これは、hashmap_fast.c
内のジェネリックなコードが、この型に対してハッシュ遅延最適化を適用すべきかどうかを判断するために使用されます。
src/pkg/runtime/hashmap_fast.c
の変更
このファイルは、Goのマップルックアップの高速パスを実装している部分です。HASH_LOOKUP1
とHASH_LOOKUP2
というジェネリックなマクロ関数が定義されており、これらがhashmap.c
で特定のキー型(uint32
, uint64
, String
など)向けにインスタンス化されます。
主な変更点は、単一バケット(h->B == 0
)の場合の処理ロジックです。
--- a/src/pkg/runtime/hashmap_fast.c
+++ b/src/pkg/runtime/hashmap_fast.c
@@ -19,10 +19,12 @@ HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)\n uintptr hash;\n uintptr bucket, oldbucket;\n Bucket *b;\n-\tuint8 top;\n \tuintptr i;\n \tKEYTYPE *k;\n \tbyte *v;\n+\tuint8 top;\n+\tint8 keymaybe;\n+\tbool quickkey;\
\n \tif(debug) {\n \t\truntime·prints(\"runtime.mapaccess1_fastXXX: map=\");\n@@ -41,17 +43,43 @@ HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)\n \tif(docheck)\n \t\tcheck(t, h);\n \n-\tif(h->B == 0 && (h->count == 1 || QUICKEQ(key))) {\n-\t\t// One-bucket table. Don\'t hash, just check each bucket entry.\n+\tif(h->B == 0) {\n+\t\t// One-bucket table. Don\'t hash, just check each bucket entry.\n+\t\tif(HASMAYBE) {\n+\t\t\tkeymaybe = -1;\n+\t\t}\n+\t\tquickkey = QUICKEQ(key);\
\t\tb = (Bucket*)h->buckets;\n \t\tfor(i = 0, k = (KEYTYPE*)b->data, v = (byte*)(k + BUCKETSIZE); i < BUCKETSIZE; i++, k++, v += h->valuesize) {\n-\t\t\tif(b->tophash[i] != 0 && EQFUNC(key, *k)) {\n-\t\t\t\tvalue = v;\n+\t\t\t\tif(b->tophash[i] != 0) {\n+\t\t\t\t\tif(quickkey && EQFUNC(key, *k)) {\n+\t\t\t\t\t\tvalue = v;\n+\t\t\t\t\t\tFLUSH(&value);\n+\t\t\t\t\t\treturn;\n+\t\t\t\t\t}\n+\t\t\t\t\tif(HASMAYBE && EQMAYBE(key, *k)) {\n+\t\t\t\t\t\t// TODO: check if key.str matches. Add EQFUNCFAST?\n+\t\t\t\t\t\tif(keymaybe >= 0) {\n+\t\t\t\t\t\t\t// Two same-length strings in this bucket.\n+\t\t\t\t\t\t\t// use slow path.\n+\t\t\t\t\t\t\t// TODO: keep track of more than just 1. Especially\n+\t\t\t\t\t\t\t// if doing the TODO above.\n+\t\t\t\t\t\t\tgoto dohash;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tkeymaybe = i;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif(HASMAYBE && keymaybe >= 0) {\n+\t\t\t\tk = (KEYTYPE*)b->data + keymaybe;\n+\t\t\t\tif(EQFUNC(key, *k)) {\n+\t\t\t\t\tvalue = (byte*)((KEYTYPE*)b->data + BUCKETSIZE) + keymaybe * h->valuesize;\n \t\t\t\tFLUSH(&value);\n \t\t\t\treturn;\n \t\t\t}\n \t\t}\n \t} else {\n+dohash:\n \t\thash = h->hash0;\n \t\tHASHFUNC(&hash, sizeof(KEYTYPE), &key);\n \t\tbucket = hash & (((uintptr)1 << h->B) - 1);\
keymaybe
とquickkey
変数の追加:keymaybe
:HASMAYBE
がtrue
の場合(つまり文字列キーの場合)、同じ長さのキーがバケット内に見つかったインデックスを保持します。初期値は-1
です。quickkey
:QUICKEQ(key)
の結果を保持します。これは、キーが非常に短い場合にtrue
となり、直接EQFUNC
で比較できる可能性を示します。
- 単一バケット処理のロジック変更:
if(h->B == 0)
: 単一バケットのマップの場合の処理ブロックです。- ループ内で各エントリをチェックする際、まず
b->tophash[i] != 0
(エントリが空ではない)ことを確認します。 if(quickkey && EQFUNC(key, *k))
:quickkey
がtrue
(短い文字列)で、かつEQFUNC
(完全な等価性比較)が一致した場合、ハッシュ計算なしで値が見つかったと判断し、即座にリターンします。if(HASMAYBE && EQMAYBE(key, *k))
:HASMAYBE
がtrue
(文字列キー)で、かつEQMAYBE
(長さが一致)した場合の処理です。if(keymaybe >= 0)
: もし既に同じ長さのキーが1つ見つかっていた場合、これは同じ長さのキーが複数存在することを意味します。この場合、ハッシュ計算をスキップする最適化は適用できないため、goto dohash;
で通常のハッシュ計算パスにフォールバックします。keymaybe = i;
: 同じ長さのキーが初めて見つかった場合、そのインデックスをkeymaybe
に保存します。
- ループ終了後、
if(HASMAYBE && keymaybe >= 0)
: もしHASMAYBE
がtrue
で、かつkeymaybe
が有効なインデックス(つまり、同じ長さのキーが1つだけ見つかった)の場合、そのキーに対してのみEQFUNC
(完全な等価性比較)を実行し、一致すれば値を返します。ここでもハッシュ計算は行われません。
dohash
ラベルの追加: 通常のハッシュ計算とバケットルックアップのパスへのジャンプポイントとしてdohash:
ラベルが追加されています。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、Goのマップルックアップの内部ロジック、特にsrc/pkg/runtime/hashmap_fast.c
内のHASH_LOOKUP1
およびHASH_LOOKUP2
マクロ関数に集約されています。これらの関数は、マップからキーに対応する値を取得する際の高速パスを提供します。
変更の目的と動作原理
この変更の主な目的は、要素数が少ない文字列マップ(具体的には2〜8個のキーを持つマップ)において、キーのハッシュ計算を可能な限り回避し、ルックアップ性能を向上させることです。
Goのマップは、要素数が少ない場合、すべてのキーが単一のバケットに格納されるように設計されています。この「単一バケット」の状態(h->B == 0
)が、この最適化の前提条件となります。
新しいロジックは以下のステップで動作します。
- 単一バケットの判定: まず、現在のマップが単一バケットであるかどうか(
h->B == 0
)をチェックします。 - 文字列キーの特殊処理:
HASMAYBE
マクロがtrue
(つまり、キーが文字列型)の場合、keymaybe
変数を-1
で初期化します。これは、同じ長さのキーがバケット内で見つかったかどうか、そしてそれが唯一であるかどうかを追跡するためのフラグです。quickkey
変数は、ルックアップ対象のキーがQUICKEQ
マクロ(短い文字列の場合にtrue
)によって高速比較可能かどうかを示します。
- バケット内のエントリ走査:
- バケット内の各エントリ(キーと値のペア)をループで走査します。
- 即時一致のチェック:
if(b->tophash[i] != 0)
(エントリが空ではない)かつif(quickkey && EQFUNC(key, *k))
の場合、つまり、ルックアップ対象のキーが短く、かつ完全な等価性比較(EQFUNC
)で一致した場合、ハッシュ計算なしで値が見つかったと判断し、即座に処理を終了します。これは最も高速なパスです。 - 長さ一致のチェック(ハッシュ遅延の可能性):
if(HASMAYBE && EQMAYBE(key, *k))
の場合、つまり、キーが文字列型で、かつルックアップ対象のキーとバケット内のキーの長さが一致した場合、以下の処理を行います。if(keymaybe >= 0)
: もし既にkeymaybe
が設定されている(つまり、同じ長さのキーが既に1つ見つかっている)場合、これは同じ長さのキーが複数存在することを意味します。この状況では、ハッシュ計算をスキップする最適化は安全に適用できないため、goto dohash;
によって通常のハッシュ計算パスにフォールバックします。keymaybe = i;
: 同じ長さのキーが初めて見つかった場合、そのエントリのインデックスi
をkeymaybe
に保存します。
- ハッシュ遅延による最終比較:
- ループが終了した後、
if(HASMAYBE && keymaybe >= 0)
をチェックします。これは、キーが文字列型であり、かつバケット内でルックアップ対象のキーと同じ長さを持つキーが唯一見つかった場合にtrue
となります。 - この場合、
keymaybe
が指すキーに対してのみEQFUNC
(完全な等価性比較)を実行します。ここで一致すれば、ハッシュ計算を全く行わずに値を取得できます。
- ループが終了した後、
- 通常のハッシュ計算へのフォールバック:
- 上記のどの条件にも合致しない場合(例: マップが単一バケットではない、文字列キーではない、同じ長さの文字列キーが複数ある、など)、
dohash:
ラベルにジャンプし、通常のハッシュ計算とバケットルックアップのロジックが実行されます。
- 上記のどの条件にも合致しない場合(例: マップが単一バケットではない、文字列キーではない、同じ長さの文字列キーが複数ある、など)、
なぜこれが高速化につながるのか
- 文字列ハッシュのコスト削減: 文字列のハッシュ計算は、文字列の長さに比例してコストが増大します。この最適化により、特に短い文字列や、同じ長さの文字列が少ない場合に、このコストの高い操作を完全に回避できます。
- キャッシュ効率の向上: ハッシュ計算をスキップすることで、CPUのキャッシュミスが減少し、メモリアクセスの効率が向上する可能性があります。
- 分岐予測の改善: 特定の条件(単一バケット、短い文字列、長さの一致)が頻繁に発生する場合、CPUの分岐予測が成功しやすくなり、パイプラインのストールが減少します。
この最適化は、Goのマップが内部的にどのように動作し、どのようなボトルネックが存在するかを深く理解した上で設計された、非常に効果的なパフォーマンス改善策と言えます。特に、BenchmarkMapStringKeysEight_1M
のような劇的な改善は、この最適化が特定のワークロードにおいて極めて大きな影響を与えることを示しています。
関連リンク
- Go言語のマップの内部実装に関する公式ドキュメントやブログ記事(当時のものがあれば)
- Go言語のランタイムソースコード(
src/runtime/map.go
やsrc/runtime/hashmap.go
など、現在の実装) - Go言語のベンチマークに関するドキュメント
参考にした情報源リンク
- Go言語の公式ドキュメント (Go map)
- Go言語のソースコード (Go runtime)
- Go言語のベンチマークに関する記事