[インデックス 19030] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc
)とランタイム(runtime
)において、map[string]
型へのルックアップ時に、キーが[]byte
スライスからstring(b)
のように変換されて生成される場合に、その変換に伴う不要なメモリ割り当てを最適化するものです。これにより、特にパフォーマンスが重要な場面でのガベージコレクションの負荷を軽減し、実行速度を向上させます。
コミット
commit f5f5a8b6209f84961687d993b93ea0d397f5d5bf
Author: Russ Cox <rsc@golang.org>
Date: Thu Apr 3 19:05:17 2014 -0400
cmd/gc, runtime: optimize map[string] lookup from []byte key
Brad has been asking for this for a while.
I have resisted because I wanted to find a more general way to
do this, one that would keep the performance of code introducing
variables the same as the performance of code that did not.
(See golang.org/issue/3512#c20).
I have not found the more general way, and recent changes to
remove ambiguously live temporaries have blown away the
property I was trying to preserve, so that's no longer a reason
not to make the change.
Fixes #3512.
LGTM=iant
R=iant
CC=bradfitz, golang-codereviews, khr, r
https://golang.org/cl/83740044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f5f5a8b6209f84961687d993b93ea0d397f5d5bf
元コミット内容
cmd/gc, runtime: optimize map[string] lookup from []byte key
Brad has been asking for this for a while.
I have resisted because I wanted to find a more general way to
do this, one that would keep the performance of code introducing
variables the same as the performance of code that did not.
(See golang.org/issue/3512#c20).
I have not found the more general way, and recent changes to
remove ambiguously live temporaries have blown away the
property I was trying to preserve, so that's no longer a reason
not to make the change.
Fixes #3512.
LGTM=iant
R=iant
CC=bradfitz, golang-codereviews, khr, r
https://golang.org/cl/83740044
変更の背景
この変更の背景には、Go言語におけるmap[string]
へのキーとして[]byte
スライスをstring(b)
のように文字列に変換して使用する際の、潜在的なパフォーマンス問題がありました。通常、[]byte
からstring
への変換は、新しい文字列データのためのメモリ割り当てを伴います。これは、マップルックアップのたびに一時的な文字列オブジェクトが生成され、ガベージコレクションの対象となることを意味します。
Brad Fitzpatrick氏(Goチームの著名なメンバー)は、この最適化を以前から求めていました。コミットの著者であるRuss Cox氏は、より汎用的な最適化方法を見つけたいと考えており、変数を導入するコードとそうでないコードのパフォーマンスを同等に保つことを目指していました(golang.org/issue/3512#c20
で言及されています)。しかし、より汎用的な方法が見つからず、また最近のコンパイラの変更(曖昧な生存期間を持つ一時変数の削除)によって、Russ Cox氏が保持しようとしていた特性が失われたため、この特定の最適化を導入する障壁がなくなったと判断されました。
この最適化は、特にネットワークプログラミングやデータ処理など、[]byte
形式のデータを頻繁に文字列キーとしてマップに利用するアプリケーションにおいて、ガベージコレクションの頻度と負荷を大幅に軽減し、全体的なパフォーマンスを向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と内部動作に関する知識が必要です。
-
Goの
string
型と[]byte
型:- Goの
string
型は不変(immutable)なバイト列です。一度作成されると内容を変更できません。 []byte
型は可変(mutable)なバイトスライスです。内容を変更できます。string(b []byte)
のような型変換は、通常、b
のバイト列をコピーして新しいstring
を作成します。これはメモリ割り当てを伴います。
- Goの
-
Goの
map
(ハッシュマップ):- Goの
map
はキーと値のペアを格納するデータ構造です。 map
のキーは比較可能(comparable)な型である必要があります。string
は比較可能ですが、[]byte
は直接比較可能ではないため、map[[]byte]T
のようなマップはGoでは定義できません。map
へのルックアップは、キーのハッシュ値を計算し、そのハッシュ値に基づいて内部のバケットを検索することで行われます。
- Goの
-
ガベージコレクション(GC)とメモリ割り当て:
- Goは自動メモリ管理(ガベージコレクション)を採用しています。
- プログラムが新しいオブジェクトを生成するたびにメモリが割り当てられます。これらのオブジェクトが不要になると、ガベージコレクタがそれらを回収し、メモリを解放します。
- 頻繁な小さなメモリ割り当ては、ガベージコレクタの実行頻度を高め、プログラムの実行を一時停止させる(ストップ・ザ・ワールド)原因となり、パフォーマンスに悪影響を与える可能性があります。
-
Goコンパイラ(
cmd/gc
)とランタイム(runtime
):cmd/gc
はGoの公式コンパイラです。Goのソースコードを機械語に変換する際に、最適化を行います。runtime
はGoプログラムの実行を管理する部分です。ガベージコレクション、スケジューリング、マップ操作などの低レベルな機能を提供します。- コンパイラとランタイムは密接に連携しており、コンパイラが特定のランタイム関数を呼び出すコードを生成したり、ランタイムがコンパイラによって生成されたコードの実行をサポートしたりします。
-
testing.AllocsPerRun
:- Goの標準ライブラリ
testing
パッケージに含まれるベンチマークヘルパー関数です。 - 指定された関数が実行されるたびに発生するメモリ割り当ての平均回数を測定するために使用されます。このコミットでは、最適化が実際にメモリ割り当てを削減したことを検証するために利用されています。
- Goの標準ライブラリ
-
エスケープ解析(Escape Analysis):
- コンパイラ最適化の一種で、変数がヒープに割り当てられるべきか(エスケープするか)、スタックに割り当てられるべきか(エスケープしないか)を決定します。
- スタック割り当てはヒープ割り当てよりも高速で、ガベージコレクションの対象にならないため、パフォーマンス向上に寄与します。
- このコミットの背景でRuss Cox氏が言及している「曖昧な生存期間を持つ一時変数の削除」は、エスケープ解析に関連するコンパイラの変更を指している可能性があります。
技術的詳細
このコミットの核心は、map[string]T
へのルックアップにおいて、キーがstring([]byte)
の形式で与えられた場合に、一時的なstring
オブジェクトの生成を回避する最適化です。
従来の動作では、m[string(buf)]
のようなコードが実行されると、buf
([]byte
)の内容がコピーされて新しいstring
が作成され、そのstring
がマップルックアップのキーとして使用されていました。この新しいstring
は一時的なオブジェクトであり、ルックアップが完了するとガベージコレクションの対象となります。
この最適化では、コンパイラ(cmd/gc
)がmap
ルックアップのコンテキストでstring([]byte)
変換を検出した場合、特別なランタイム関数runtime.slicebytetostringtmp
を呼び出すようにコードを生成します。
runtime.slicebytetostringtmp
関数の特徴は以下の通りです。
- メモリ割り当ての回避: この関数は、
[]byte
スライスの基盤となる配列をコピーせず、その配列を直接参照するstring
ヘッダ(ポインタと長さ)を作成します。これにより、新しいメモリ割り当てが発生しません。 - 一時的な性質: 生成される
string
は「一時的(temporary)」なものとして扱われます。これは、そのstring
がマップルックアップのためだけに存在し、ルックアップが完了する前に元の[]byte
スライスが変更されたり、他のゴルーチンと同期されたりしないことが保証される場合にのみ安全に使用できることを意味します。コンパイラは、このstring
がすぐに使用され、その後破棄されることを知っているため、この最適化を適用できます。 - 特定のユースケースに限定: コミットメッセージにもあるように、この最適化は「
m[string(k)]
wherem
is a string-keyed map andk
is a[]byte
」という特定のケースに限定されます。これは、[]byte
をキーとするマップが存在しないため、このケースが実用上重要であるためです。
この最適化を実現するために、コンパイラとランタイムに以下の変更が加えられました。
src/cmd/gc/builtin.c
: ランタイムの組み込み関数リストにslicebytetostringtmp
が追加されました。src/cmd/gc/go.h
: コンパイラの内部表現で、一時的なstring([]byte)
変換を表す新しいオペレーションコードOARRAYBYTESTRTMP
が追加されました。src/cmd/gc/order.c
: コンパイラの順序付けフェーズ(orderstmt
、orderexpr
)で、map
ルックアップの右辺値がOARRAYBYTESTR
(通常のstring([]byte)
変換)である場合に、それをOARRAYBYTESTRTMP
に書き換えるロジックが追加されました。これにより、コンパイラは後続のフェーズでこの特殊なケースを認識できます。src/cmd/gc/runtime.go
:slicebytetostringtmp
関数のシグネチャがGoコンパイラに認識されるように宣言されました。src/cmd/gc/walk.c
: コンパイラのウォークフェーズ(walkexpr
)で、OARRAYBYTESTRTMP
オペレーションが検出された場合に、runtime.slicebytetostringtmp
ランタイム関数を呼び出すコードを生成するように変更されました。src/pkg/runtime/string.goc
:runtime.slicebytetostringtmp
関数の実際のC実装が追加されました。この実装は、[]byte
スライスのポインタと長さを直接string
構造体に設定することで、メモリコピーを回避します。また、raceenabled
の場合にはデータ競合検出のためのracereadrangepc
呼び出しも含まれています。src/pkg/runtime/map_test.go
: この最適化が実際にメモリ割り当てを削減していることを検証するためのベンチマークテストTestMapStringBytesLookup
が追加されました。testing.AllocsPerRun
を使用して、string([]byte)
変換を伴うマップルックアップが0回のメモリ割り当てで実行されることを確認しています。
この最適化は、Goのコンパイラがコードのセマンティクスを維持しつつ、特定のパターンを認識してより効率的なランタイム関数に置き換える能力を示す良い例です。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/gc/builtin.c
:--- a/src/cmd/gc/builtin.c +++ b/src/cmd/gc/builtin.c @@ -33,6 +33,7 @@ char *runtimeimport = "func @\".eqstring (? string, ? string) (? bool)\n" "func @\".intstring (? int64) (? string)\n" "func @\".slicebytetostring (? []byte) (? string)\n" + "func @\".slicebytetostringtmp (? []byte) (? string)\n" "func @\".slicerunetostring (? []rune) (? string)\n" "func @\".stringtoslicebyte (? string) (? []byte)\n" "func @\".stringtoslicerune (? string) (? []rune)\n"
runtime.slicebytetostringtmp
関数の宣言を追加。 -
src/cmd/gc/go.h
:--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -450,6 +450,7 @@ enum OANDAND, // b0 && b1 OAPPEND, // append OARRAYBYTESTR, // string(bytes) + OARRAYBYTESTRTMP, // string(bytes) ephemeral OARRAYRUNESTR, // string(runes) OSTRARRAYBYTE, // []byte(s) OSTRARRAYRUNE, // []rune(s)
新しいコンパイラ内部オペレーション
OARRAYBYTESTRTMP
を追加。 -
src/cmd/gc/order.c
:--- a/src/cmd/gc/order.c +++ b/src/cmd/gc/order.c @@ -565,9 +565,13 @@ orderstmt(Node *n, Order *order) // and make sure OINDEXMAP is not copied out. t = marktemp(order); orderexprlist(n->list, order); - orderexpr(&n->rlist->n->left, order); - orderexpr(&n->rlist->n->right, order); - orderaddrtemp(&n->rlist->n->right, order); + r = n->rlist->n; + orderexpr(&r->left, order); + orderexpr(&r->right, order); + // See case OINDEXMAP below. + if(r->right->op == OARRAYBYTESTR) + r->right->op = OARRAYBYTESTRTMP; + orderaddrtemp(&r->right, order); ordermapassign(n, order); cleantemp(t, order); break; @@ -935,6 +939,20 @@ orderexpr(Node **np, Order *order) // key must be addressable orderexpr(&n->left, order); orderexpr(&n->right, order); + + // For x = m[string(k)] where k is []byte, the allocation of + // backing bytes for the string can be avoided by reusing + // the []byte backing array. This is a special case that it + // would be nice to handle more generally, but because + // there are no []byte-keyed maps, this specific case comes + // up in important cases in practice. See issue 3512. + // Nothing can change the []byte we are not copying before + // the map index, because the map access is going to + // be forced to happen immediately following this + // conversion (by the ordercopyexpr a few lines below). + if(n->etype == 0 && n->right->op == OARRAYBYTESTR) + n->right->op = OARRAYBYTESTRTMP; + orderaddrtemp(&n->right, order); if(n->etype == 0) { // use of value (not being assigned);
map
ルックアップのコンテキストでstring([]byte)
変換(OARRAYBYTESTR
)をOARRAYBYTESTRTMP
に変換するロジックを追加。 -
src/cmd/gc/walk.c
:--- a/src/cmd/gc/walk.c +++ b/src/cmd/gc/walk.c @@ -1316,6 +1316,11 @@ walkexpr(Node **np, NodeList **init) n = mkcall("slicebytetostring", n->type, init, n->left); goto ret; + case OARRAYBYTESTRTMP: + // slicebytetostringtmp([]byte) string; + n = mkcall("slicebytetostringtmp", n->type, init, n->left); + goto ret; + case OARRAYRUNESTR: // slicerunetostring([]rune) string; n = mkcall("slicerunetostring", n->type, init, n->left);
OARRAYBYTESTRTMP
オペレーションをruntime.slicebytetostringtmp
関数呼び出しに変換するコード生成ロジックを追加。 -
src/pkg/runtime/string.goc
:--- a/src/pkg/runtime/string.goc +++ b/src/pkg/runtime/string.goc @@ -294,6 +294,25 @@ func slicebytetostring(b Slice) (s String) { runtime·memmove(s.str, b.array, s.len); } +func slicebytetostringtmp(b Slice) (s String) { + void *pc; + + if(raceenabled) { + pc = runtime·getcallerpc(&b); + runtime·racereadrangepc(b.array, b.len, pc, runtime·slicebytetostringtmp); + } + + // Return a "string" referring to the actual []byte bytes. + // This is only for use by internal compiler optimizations + // that know that the string form will be discarded before + // the calling goroutine could possibly modify the original + // slice or synchronize with another goroutine. + // Today, the only such case is a m[string(k)] lookup where + // m is a string-keyed map and k is a []byte. + s.str = b.array; + s.len = b.len; +} + func stringtoslicebyte(s String) (b Slice) { uintptr cap;
runtime.slicebytetostringtmp
関数のC実装を追加。これはメモリコピーを行わず、元の[]byte
のバッキング配列を直接参照するstring
を作成します。 -
src/pkg/runtime/map_test.go
:--- a/src/pkg/runtime/map_test.go +++ b/src/pkg/runtime/map_test.go @@ -438,3 +438,40 @@ func TestMapIterOrder(t *testing.T) { } } +\n+func TestMapStringBytesLookup(t *testing.T) { +\t// Use large string keys to avoid small-allocation coalescing, +\t// which can cause AllocsPerRun to report lower counts than it should. +\tm := map[string]int{ +\t\t"1000000000000000000000000000000000000000000000000": 1, +\t\t"2000000000000000000000000000000000000000000000000": 2, +\t} +\tbuf := []byte("1000000000000000000000000000000000000000000000000") +\tif x := m[string(buf)]; x != 1 { +\t\tt.Errorf(`m[string([]byte("1"))] = %d, want 1`, x) +\t}\n+\tbuf[0] = '2' +\tif x := m[string(buf)]; x != 2 { +\t\tt.Errorf(`m[string([]byte("2"))] = %d, want 2`, x) +\t}\n+\n+\tvar x int +\tn := testing.AllocsPerRun(100, func() { +\t\tx += m[string(buf)] +\t})\n+\tif n != 0 { +\t\tt.Errorf("AllocsPerRun for m[string(buf)] = %v, want 0", n) +\t}\n+\n+\tx = 0 +\tn = testing.AllocsPerRun(100, func() { +\t\ty, ok := m[string(buf)] +\t\tif !ok { +\t\t\tpanic("!ok") +\t\t}\n+\t\tx += y +\t})\n+\tif n != 0 { +\t\tt.Errorf("AllocsPerRun for x,ok = m[string(buf)] = %v, want 0", n) +\t}\n+}
最適化の有効性を検証するためのベンチマークテスト
TestMapStringBytesLookup
を追加。
コアとなるコードの解説
-
src/cmd/gc/builtin.c
とsrc/cmd/gc/go.h
の変更: これらはコンパイラが新しいランタイム関数slicebytetostringtmp
と、それに対応する内部オペレーションOARRAYBYTESTRTMP
を認識できるようにするための定義です。コンパイラがコードを解析し、最適化を適用する際にこれらのシンボルが必要になります。 -
src/cmd/gc/order.c
の変更: このファイルはコンパイラの「順序付け(ordering)」フェーズを担当します。ここでは、式の評価順序を決定し、一時変数を導入するなどの変換を行います。 追加されたロジックは、map
のインデックス操作(OINDEXMAP
)の右辺値がstring([]byte)
変換(OARRAYBYTESTR
)である場合に、そのオペレーションをOARRAYBYTESTRTMP
に書き換えます。これは、コンパイラがこの特定のパターンを認識し、後続のフェーズで特別な処理を適用するための「マーク付け」のようなものです。 コメントにあるように、「Nothing can change the []byte we are not copying before the map index, because the map access is going to be forced to happen immediately following this conversion
」という保証があるため、この一時的な文字列参照が安全に使用できると判断されます。 -
src/cmd/gc/walk.c
の変更: このファイルはコンパイラの「ウォーク(walking)」フェーズを担当します。ここでは、抽象構文木(AST)を走査し、高レベルなGoの操作を低レベルなランタイム関数呼び出しに変換します。OARRAYBYTESTRTMP
オペレーションが検出されると、通常のslicebytetostring
ではなく、新しく導入されたslicebytetostringtmp
ランタイム関数を呼び出すコードを生成するように変更されています。これにより、メモリ割り当てを伴わない文字列変換が実現されます。 -
src/pkg/runtime/string.goc
の変更: このファイルはGoランタイムの文字列関連の低レベルなCコードを含んでいます。slicebytetostringtmp
関数の実装が追加されています。この関数は、引数として受け取った[]byte
スライスb
の基盤となるデータポインタ(b.array
)と長さ(b.len
)を、新しいstring
構造体s
の対応するフィールド(s.str
とs.len
)に直接コピーします。これにより、バイト列の実際のコピーは行われず、メモリ割り当てが回避されます。raceenabled
(データ競合検出が有効な場合)のチェックも含まれており、racereadrangepc
を呼び出して、この一時的な文字列参照がデータ競合を引き起こさないことを保証します。 -
src/pkg/runtime/map_test.go
の変更: このテストは、最適化が正しく機能していることを検証するためのものです。testing.AllocsPerRun
を使用して、map[string]int
へのルックアップでstring([]byte)
変換を使用した場合のメモリ割り当て回数を測定しています。最適化が成功していれば、この操作は0回のメモリ割り当てで実行されるはずです。これにより、ガベージコレクションの負荷が軽減されることが実証されます。
これらの変更が連携することで、特定のmap
ルックアップパターンにおいて、コンパイラが賢く動作し、不要なメモリ割り当てを排除してパフォーマンスを向上させています。
関連リンク
- Go Issue 3512: https://github.com/golang/go/issues/3512
このコミットが修正したGoのイシューです。
map[string]
ルックアップにおける[]byte
キーからの最適化に関する議論が含まれています。 - Go Code Review 83740044: https://golang.org/cl/83740044 このコミットに対応するGoのコードレビューページです。変更の詳細な議論やレビューコメントが含まれています。
参考にした情報源リンク
- コミットメッセージ自体
- Go Issue 3512の議論
- Go Code Review 83740044の議論
- Go言語の
string
と[]byte
の変換に関する一般的なドキュメント(Goの公式ドキュメントやブログ記事など) - Goのガベージコレクションとメモリ管理に関する一般的な情報
testing.AllocsPerRun
のドキュメント- Goコンパイラの内部構造に関する一般的な情報(
cmd/gc
の各フェーズなど) - Web検索結果: "golang issue 3512 map string lookup byte key" (Google Search)
- https://github.com/golang/go/issues/3512
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEyPEyN9fp2m5E40ffVKgh7UPn31cwWeJy1h3q6xbwu2EM7REAWmcbptnq9nyLthi3qAV-k9AYK2eyBYcO3p0Nqb-419dppH6Sxsk85F-bb1sutcZ7PsFU3ianI9JZMyGbs9Q==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGLd6JgSHTj-2nH2rzn3DmDV2ZwMmwcNQmC34rdLRALPAeOFUTA5AW1W0rBVPAiAlTGSyJFN3-DGp2uupq7cOPELdeymOC4kzk4Y99Oz5V-Y6mn8LYkRfyDt6YhbEjDjKjz6P9PbageAMReJQXCfYOPGLMnFpk0mHKp7oyvF3gWsaM5bUi94hFRBvxtdO6Lii1NIvjZ0ix5zzPhEqY=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHNzAc6SxXq_mg99c7JAKEd63d1Ye2zu9fOhIic2XtPnzjlXbovQtBznSub86oeoD8i1kWvND9bs_yY7hwiUq9kP6JBucaLLafX-wtHK9ug5YefRLJIBbMvvZd1-VZqJ_w0WrAFB6FZ84WE8CdnAEbVtUvN7maflhWHY3XyVZTthGsmqVAEw==