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

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

このコミットは、Go言語の標準ライブラリであるstrconvパッケージのテストコードstrconv_test.goにおける変更です。具体的には、AppendIntおよびAppendFloat関数が特定の条件下でメモリ割り当てを行わなくなったことを反映し、テストケースの期待値を更新するとともに、関連するTODOコメントを削除しています。これは、Goコンパイラのエスケープ解析の改善により、これらの関数呼び出しにおける不要なヒープ割り当てが解消されたことを示しています。

コミット

commit b6a39a25455b07b98a48b97ebfb761fb080af825
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Apr 4 17:04:18 2013 -0700

    strconv: remove some test TODOs and adjust malloc limits lower
    
    These no longer allocate.
    
    R=golang-dev, dave
    CC=golang-dev
    https://golang.org/cl/8340047

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

https://github.com/golang/go/commit/b6a39a25455b07b98a48b97ebfb761fb080af825

元コミット内容

strconv: いくつかのテストTODOを削除し、mallocの制限を低く調整 これらはもはや割り当てを行わない。

変更の背景

このコミットの背景には、Go言語のコンパイラにおけるエスケープ解析の継続的な改善があります。Goのコンパイラは、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定するためにエスケープ解析を実行します。スタック割り当てはヒープ割り当てよりも高速であり、ガベージコレクション(GC)の負担を軽減するため、パフォーマンスの最適化において非常に重要です。

以前のGoコンパイラでは、strconvパッケージのAppendIntAppendFloatのような関数が、ローカルに宣言されたバッファのスライス(例: localBuf[:0])を引数として受け取る場合でも、不必要にヒープ割り当て(malloc)を発生させてしまうケースがありました。これは、コンパイラのエスケープ解析が、これらのスライスが関数呼び出し後も生存し続ける可能性があると誤って判断していたためです。

このコミットが行われた2013年4月頃は、Go言語が活発に開発され、コンパイラの最適化、特にエスケープ解析の精度向上が進められていた時期です。この改善により、AppendIntAppendFloatがローカルバッファを使用する際に、コンパイラがそのスライスが関数スコープ内で完結し、ヒープにエスケープしないことを正確に判断できるようになりました。その結果、これらの操作におけるヒープ割り当てが不要となり、テストコードがその事実を反映するように更新されました。

前提知識の解説

Go言語のstrconvパッケージ

strconvパッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(整数、浮動小数点数、真偽値など)と文字列との間の変換機能を提供します。例えば、Atoiは文字列を整数に、Itoaは整数を文字列に変換します。 このコミットで関連するのは、AppendIntAppendFloatといった関数です。これらは、既存のバイトスライスに数値を文字列として追加する機能を提供します。例えば、AppendInt([]byte("Value: "), 123, 10)[]byte("Value: 123")を返します。

メモリ割り当て(Memory Allocation)

プログラムが実行時にデータを格納するためにメモリを確保するプロセスです。Go言語では、主に以下の2種類のメモリ領域が使用されます。

  1. スタック(Stack): 関数呼び出しやローカル変数など、生存期間が短いデータのために使用されるメモリ領域です。LIFO(Last-In, First-Out)の構造を持ち、高速な割り当てと解放が可能です。スタックに割り当てられたメモリは、関数が終了すると自動的に解放されます。
  2. ヒープ(Heap): プログラムの実行中に動的に割り当てられ、生存期間が関数スコールを超えても続く可能性があるデータのために使用されるメモリ領域です。ヒープに割り当てられたメモリは、ガベージコレクタ(GC)によって管理され、不要になった時点で解放されます。ヒープ割り当てはスタック割り当てよりもオーバーヘッドが大きく、GCの実行はプログラムのパフォーマンスに影響を与える可能性があります。

エスケープ解析(Escape Analysis)

Goコンパイラが行う重要な最適化の一つです。コンパイラは、プログラム内の変数がヒープに割り当てられるべきか(「エスケープする」と呼ばれる)、それともスタックに割り当てられるべきか(「エスケープしない」と呼ばれる)を静的に分析します。

  • エスケープする(Escapes): 変数がその宣言されたスコープ(通常は関数)を超えて生存する必要がある場合、例えば、ポインタが関数から返されたり、グローバル変数に代入されたりする場合、その変数はヒープに割り当てられます。
  • エスケープしない(Does not escape): 変数がその宣言されたスコープ内でのみ使用され、関数が終了すると不要になる場合、その変数はスタックに割り当てられます。

エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ヒープ割り当ての数を減らし、ガベージコレクションの頻度と時間を削減し、プログラムの実行速度を向上させることです。

GoにおけるAppend関数の挙動とスライス

Goのスライスは、基盤となる配列への参照、長さ、容量を持つデータ構造です。append組み込み関数は、スライスに要素を追加するために使用されます。 AppendIntAppendFloatのような関数は、内部的にappendと同様のロジックを使用することが多いです。 localBuf[:0]のような表現は、localBufという既存の配列(この場合は[64]byte)を基盤とする、長さ0のスライスを作成します。このスライスは、基盤となる配列の容量(64バイト)を再利用できるため、その容量内で要素を追加する限り、新たなメモリ割り当ては発生しないことが期待されます。

技術的詳細

このコミットの技術的詳細の核心は、Goコンパイラのエスケープ解析の進化にあります。 コミットメッセージにある「These no longer allocate.」という記述は、strconv.AppendIntおよびstrconv.AppendFloat関数が、特定の呼び出しパターンにおいて、以前は発生していたヒープ割り当て(malloc)を、もはや行わなくなったことを明確に示しています。

具体的には、strconv_test.goのテストケースでは、AppendInt(localBuf[:0], 123, 10)AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)のように、スタック上に確保されたローカル配列localBufを基盤とするスライス(localBuf[:0])をこれらの関数に渡しています。

以前のコンパイラでは、このような呼び出しにおいて、localBuf[:0]が関数内でどのように扱われるかについて、エスケープ解析が十分に賢くなかった可能性があります。例えば、コンパイラが誤って、関数内で生成された結果スライスが関数呼び出し後も生存し続ける(つまり、ヒープに「エスケープ」する)と判断し、不必要なヒープ割り当てを挿入していたことが考えられます。テストコードのallocs: 1という記述は、まさにその「1回のヒープ割り当て」が発生していたことを示しています。

しかし、このコミットが適用された時点では、Goコンパイラのエスケープ解析が改善され、より正確な分析が可能になりました。コンパイラは、AppendIntAppendFloatlocalBuf[:0]を受け取り、その基盤となる配列の容量内で操作を完結させ、結果のスライスが関数スコープ外にエスケープしないことを正しく認識できるようになりました。これにより、ヒープ割り当てが不要となり、操作が完全にスタック上で実行されるようになりました。テストコードのallocs: 0への変更は、この最適化が実際に機能していることを検証しています。

この最適化は、strconvパッケージのパフォーマンス向上に寄与します。ヒープ割り当てが減少することで、ガベージコレクションの頻度が減り、全体的な実行速度が向上します。特に、数値と文字列の変換が頻繁に行われるようなアプリケーションでは、この変更が大きな影響を与える可能性があります。

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

変更はsrc/pkg/strconv/strconv_test.goファイルに集中しています。

--- a/src/pkg/strconv/strconv_test.go
+++ b/src/pkg/strconv/strconv_test.go
@@ -20,14 +20,12 @@ var (
 		tdesc  string
 		fn    func()
 	}{
-		// TODO(bradfitz): this might be 0, once escape analysis is better
-		{1, `AppendInt(localBuf[:0], 123, 10)`, func() {
+		{0, `AppendInt(localBuf[:0], 123, 10)`, func() {
 			var localBuf [64]byte
 			AppendInt(localBuf[:0], 123, 10)
 		}},
 		{0, `AppendInt(globalBuf[:0], 123, 10)`, func() { AppendInt(globalBuf[:0], 123, 10) }},\n-\t\t// TODO(bradfitz): this might be 0, once escape analysis is better
-\t\t{1, `AppendFloat(localBuf[:0], 1.23, \'g\', 5, 64)`, func() {
+		{0, `AppendFloat(localBuf[:0], 1.23, \'g\', 5, 64)`, func() {
 			var localBuf [64]byte
 			AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)
 		}},

具体的には、以下の2つのテストケースが変更されています。

  1. AppendInt(localBuf[:0], 123, 10)のテストケース:
    • 期待されるメモリ割り当て数(allocsフィールド)が1から0に変更されました。
    • 関連する// TODO(bradfitz): this might be 0, once escape analysis is betterというコメントが削除されました。
  2. AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)のテストケース:
    • 期待されるメモリ割り当て数(allocsフィールド)が1から0に変更されました。
    • 関連する// TODO(bradfitz): this might be 0, once escape analysis is betterというコメントが削除されました。

コアとなるコードの解説

この変更は、strconvパッケージのAppendIntおよびAppendFloat関数が、ローカルに宣言された固定サイズの配列(localBuf)を基盤とするスライス(localBuf[:0])を引数として受け取る場合に、ヒープ割り当てがゼロになったことをテストコードで検証しています。

memテスト関数は、各操作がどれだけのメモリ割り当て(allocs)とバイト数(bytes)を発生させるかを測定するためのものです。 変更前のテストコードでは、AppendInt(localBuf[:0], ...)AppendFloat(localBuf[:0], ...)のケースで、期待される割り当て数が1と設定されていました。これは、当時のGoコンパイラのエスケープ解析が、これらの操作で不必要に1回のヒープ割り当てを発生させていたことを示しています。同時に、// TODO(bradfitz): this might be 0, once escape analysis is betterというコメントは、将来的にエスケープ解析が改善されれば、この割り当てがゼロになる可能性があるという開発者の認識を示していました。

このコミットでは、その「将来」が現実のものとなり、コンパイラのエスケープ解析が十分に改善されたため、これらの操作がヒープ割り当てを全く行わなくなったことを確認し、テストの期待値を0に更新しています。これにより、これらの関数がローカルバッファを使用する際に、より効率的に(ヒープ割り当てなしで)動作することが保証されます。

localBufは関数内で宣言されたローカル変数であり、スタック上に割り当てられます。localBuf[:0]はそのlocalBufを基盤とするスライスであり、その容量はlocalBufのサイズ(64バイト)です。AppendIntAppendFloatがこのスライスに数値を文字列として追加する際、結果の文字列が64バイトの容量内に収まる限り、新たな基盤配列をヒープに割り当てる必要はありません。コンパイラがこの事実を正確に把握できるようになったため、不要なヒープ割り当てが排除されたのです。

関連リンク

  • Go言語のstrconvパッケージのドキュメント: https://pkg.go.dev/strconv
  • Go言語のエスケープ解析に関する一般的な情報(公式ドキュメントやブログ記事など):
    • Goのブログ記事: https://go.dev/blog/go1.1-performance (Go 1.1でのパフォーマンス改善について触れられており、エスケープ解析もその一部です)
    • Goのコンパイラに関するドキュメント: https://go.dev/doc/articles/go1.1 (Go 1.1のリリースノートでコンパイラの改善について言及されています)

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/cmd/compile/internal/gc/escape.goなど、コンパイラのエスケープ解析に関連する部分)
  • Go言語のIssue TrackerおよびChange List (CL) (コミットメッセージに記載されているhttps://golang.org/cl/8340047など)
  • Go言語に関する技術ブログやフォーラムでの議論(エスケープ解析やメモリ管理に関するもの)