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

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

このコミットは、Goランタイムにおける文字列の扱いに関する重要な変更を導入しています。具体的には、文字列の終端にヌル文字(\0)を付加する慣習を廃止することで、メモリ割り当ての効率化とデバッグの容易化を図っています。

コミット

commit 9fa9613e0b63b47a1d19c1ba50a7118304dcebae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Jan 24 22:29:01 2014 +0400

    runtime: do not zero terminate strings
    On top of "tiny allocator" (cl/38750047), reduces number of allocs by 1% on json.
    No code must rely on zero termination. So will also make debugging simpler,
    by uncovering issues earlier.
    
    json-1
    allocated                 7949686      7915766      -0.43%
    allocs                      93778        92790      -1.05%
    time                    100957795     97250949      -3.67%
    rest of the metrics are too noisy.
    
    LGTM=r
    R=golang-codereviews, r, bradfitz, iant
    CC=golang-codereviews
    https://golang.org/cl/40370061

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

https://github.com/golang/go/commit/9fa9613e0b63b47a1d19c1ba50a7118304dcebae

元コミット内容

runtime: do not zero terminate strings
On top of "tiny allocator" (cl/38750047), reduces number of allocs by 1% on json.
No code must rely on zero termination. So will also make debugging simpler,
by uncovering issues earlier.

json-1
allocated                 7949686      7915766      -0.43%
allocs                      93778        92790      -1.05%
time                    100957795     97250949      -3.67%
rest of the metrics are too noisy.

LGTM=r
R=golang-codereviews, r, bradfitz, iant
CC=golang-codereviews
https://golang.org/cl/40370061

変更の背景

このコミットの背景には、Goランタイムのメモリ管理とパフォーマンスの最適化という大きな目標があります。特に、先行する「tiny allocator」の導入と密接に関連しています。

従来のC言語の慣習では、文字列はヌル文字(\0)で終端されることが一般的でした。これは、文字列の長さを明示的に保持するのではなく、ヌル文字を見つけることで文字列の終わりを判断するためです。しかし、Go言語の文字列は、長さ情報(len)とデータへのポインタ(str)を持つ構造体(stringヘッダ)として表現されます。このため、Goの内部ではヌル終端は不要であり、むしろ余分なメモリ割り当てと初期化のオーバーヘッドを発生させていました。

このコミットの目的は以下の通りです。

  1. メモリ割り当ての効率化: 文字列の末尾にヌル文字のための1バイトを余分に割り当てる必要がなくなるため、特に短い文字列が多数生成されるようなシナリオ(例: JSONのパース)において、メモリ割り当ての回数と量を削減できます。コミットメッセージにあるように、「json-1」ベンチマークで割り当て回数が1.05%削減されています。
  2. デバッグの容易化: Goのコードがヌル終端に依存していないことを明確にすることで、もし誤ってヌル終端を前提としたC言語との連携コードなどが存在した場合、早期に問題を発見できるようになります。これにより、デバッグがよりシンプルになります。
  3. 「tiny allocator」との相乗効果: この変更は、より小さなオブジェクトの割り当てを効率化する「tiny allocator」の導入と組み合わされることで、さらに大きなパフォーマンス改善をもたらすことが期待されています。ヌル終端の廃止は、割り当てられるオブジェクトのサイズをわずかに減らすため、tiny allocatorの恩恵をより多く受けられるようになります。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

1. Go言語の文字列の内部表現

Go言語の文字列は、C言語の文字列とは異なり、単なるchar*ポインタではありません。Goの文字列は、以下の2つの要素から構成される構造体として内部的に表現されます。

  • データへのポインタ: 文字列の実際のバイトデータが格納されているメモリ領域へのポインタ。
  • 長さ: 文字列のバイト長を示す整数値。

この構造により、Goの文字列はヌル文字に依存せずに長さを知ることができ、文字列の連結やスライスなどの操作が効率的に行えます。

2. ヌル終端文字列 (Null-terminated string)

C言語やC++などの一部の言語では、文字列は文字のシーケンスの終わりにヌル文字(ASCII値0、\0)を配置することで、その終わりを示します。例えば、"hello"という文字列はメモリ上では'h', 'e', 'l', 'l', 'o', '\0'のように格納されます。文字列を処理する関数は、このヌル文字を見つけることで文字列の長さを判断します。

3. メモリ割り当て (Memory Allocation)

プログラムが実行時にメモリを必要とする際(例: 新しい文字列や構造体を作成する際)に、オペレーティングシステムやランタイムからメモリ領域を要求するプロセスです。メモリ割り当ては、パフォーマンスに大きな影響を与える可能性があります。割り当て回数を減らしたり、割り当てられるメモリ量を減らしたりすることは、プログラムの実行速度を向上させる上で重要です。

4. Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するソフトウェア層です。これには、ガベージコレクタ、スケジューラ、メモリマネージャ、プリミティブなデータ構造(文字列、スライスなど)の内部実装などが含まれます。Goプログラムのパフォーマンスは、ランタイムの効率性に大きく依存します。

5. Tiny Allocator

Goランタイムにおけるメモリ割り当ての最適化の一つで、特に小さなオブジェクト(数バイトから数百バイト程度)の割り当てを高速化するために設計されたアロケータです。小さなオブジェクトの割り当ては頻繁に発生するため、これを効率化することで全体的なパフォーマンスが向上します。ヌル終端の廃止は、文字列のサイズをわずかに減らすことで、より多くの文字列がtiny allocatorの管理対象となり、その恩恵を受けやすくなります。

技術的詳細

このコミットの技術的詳細は、Goランタイムのstring.gocファイルにおける文字列割り当てロジックの変更に集約されます。

Goの文字列は、runtime.string構造体として表現され、その実体はruntime.mallocgc関数によってヒープに割り当てられます。このmallocgcは、ガベージコレクタによって管理されるメモリを割り当てるための関数です。

変更前のコードでは、gostringsize関数(Goの文字列をCの文字列として扱う際にサイズを計算する、あるいはGoの文字列を割り当てる際に使用される内部関数)内で、文字列の長さlに加えてヌル終端文字のための1バイトを追加してメモリを割り当てていました。

// 変更前
s.str = runtime·mallocgc(l+1, 0, FlagNoScan|FlagNoZero); // l+1 バイトを割り当て
s.len = l;
s.str[l] = 0; // ヌル文字を書き込む

このコードは、lバイトの文字列データに加えて、その直後にヌル文字を配置するための追加の1バイトを確保し、そこに0を書き込んでいました。これは、Goの内部では不要な処理であり、C言語との連携(例えばgetenvのような関数がGoランタイムから呼ばれる場合)を考慮したものでした。

このコミットでは、この余分な1バイトの割り当てとヌル文字の書き込みを削除しています。

// 変更後
s.str = runtime·mallocgc(l, 0, FlagNoScan|FlagNoZero); // l バイトのみを割り当て
s.len = l;
// s.str[l] = 0; // ヌル文字の書き込みを削除

これにより、文字列の実際の長さlに厳密に一致するメモリ領域のみが割り当てられるようになります。

FlagNoScanFlagNoZeroについて:

  • FlagNoScan: このフラグは、割り当てられたメモリ領域がポインタを含まないことをガベージコレクタに伝えます。文字列はバイトのシーケンスであり、他のオブジェクトへのポインタを含まないため、このフラグが設定されます。これにより、ガベージコレクタはスキャン処理をスキップでき、パフォーマンスが向上します。
  • FlagNoZero: このフラグは、割り当てられたメモリ領域をゼロ初期化しないことを示します。通常、mallocgcは割り当てられたメモリをゼロで初期化しますが、文字列データは後で上書きされるため、ゼロ初期化は不要であり、このフラグを設定することでオーバーヘッドを削減できます。

この変更は、GoランタイムがC言語のヌル終端文字列の慣習から完全に脱却し、Goの文字列モデルに最適化されたメモリ管理を行うことを意味します。これにより、Goの文字列はよりコンパクトになり、割り当て時のオーバーヘッドが削減されます。

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

変更はsrc/pkg/runtime/string.gocファイルにあります。

--- a/src/pkg/runtime/string.goc
+++ b/src/pkg/runtime/string.goc
@@ -46,10 +46,8 @@ gostringsize(intgo l)
 
  if(l == 0)
  	return runtime·emptystring;
-// leave room for NUL for C runtime (e.g., callers of getenv)
-	s.str = runtime·mallocgc(l+1, 0, FlagNoScan|FlagNoZero);
+	s.str = runtime·mallocgc(l, 0, FlagNoScan|FlagNoZero);
  s.len = l;
-	s.str[l] = 0;
  for(;;) {
  		ms = runtime·maxstring;
  		if((uintptr)l <= ms || runtime·casp((void**)&runtime·maxstring, (void*)ms, (void*)l))

コアとなるコードの解説

このdiffは、gostringsize関数(Goの文字列を生成する際に内部的に使用される)内のメモリ割り当てロジックの変更を示しています。

  1. -// leave room for NUL for C runtime (e.g., callers of getenv): このコメントは、変更前のコードがCランタイム(例えばgetenvのような関数を呼び出す場合)のためにヌル文字のためのスペースを残していたことを説明しています。このコメントが削除されたことは、この慣習がもはや適用されないことを示しています。

  2. - s.str = runtime·mallocgc(l+1, 0, FlagNoScan|FlagNoZero);: 変更前は、文字列の長さlに1バイトを追加したサイズ(l+1)でメモリを割り当てていました。この追加の1バイトはヌル終端文字のためでした。

  3. + s.str = runtime·mallocgc(l, 0, FlagNoScan|FlagNoZero);: 変更後では、文字列の実際の長さlと全く同じサイズでメモリを割り当てています。これにより、ヌル終端文字のための余分なスペースが不要になります。

  4. - s.str[l] = 0;: 変更前は、割り当てられたメモリのl番目のインデックス(つまり、文字列データの直後)にヌル文字0を書き込んでいました。この行が削除されたことで、ヌル文字の書き込み処理自体がなくなりました。

この変更により、Goランタイムは文字列を割り当てる際に、その内容に必要な最小限のメモリのみを確保するようになります。これは、メモリ使用量の削減と、特に多数の短い文字列が生成される場合のメモリ割り当てパフォーマンスの向上に貢献します。

関連リンク

参考にした情報源リンク