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

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

このコミットは、以前に導入されたコミット CL 9805043 / 776aba85ece8 を元に戻す(undo)ものです。元に戻されたコミットは、Goランタイムに persistentalloc() というヘルパー関数を導入し、シンボルテーブル(symtab)の割り当てなどに利用することで、ビルド時間の短縮と初期ヒープサイズの削減を目指していました。しかし、この persistentalloc() の導入が amd64 アーキテクチャで複数の障害を引き起こしたため、安定性確保のためにその変更が取り消されました。

コミット

commit 828c68f8d80a642d89cc17e04aeb0116c8bce4ae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 28 11:14:39 2013 +0400

    undo CL 9805043 / 776aba85ece8
    
    multiple failures on amd64
    
    ««« original CL description
    runtime: introduce helper persistentalloc() function
    It is a caching wrapper around SysAlloc() that can allocate small chunks.
    Use it for symtab allocations. Reduces number of symtab walks from 4 to 3
    (reduces buildfuncs time from 10ms to 7.5ms on a large binary,
    reduces initial heap size by 680K on the same binary).
    Also can be used for type info allocation, itab allocation.
    There are also several places in GC where we do the same thing,
    they can be changed to use persistentalloc().
    Also can be used in FixAlloc, because each instance of FixAlloc allocates
    in 128K regions, which is too eager.
    
    R=golang-dev, daniel.morsing, khr
    CC=golang-dev
    https://golang.org/cl/9805043
    »»»
    
    R=golang-dev
    CC=golang-dev
    https://golang.org/cl/9822043

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

https://github.com/golang/go/commit/828c68f8d80a642d89cc17e04aeb0116c8bce4ae

元コミット内容

このコミットによって元に戻された CL 9805043 は、Goランタイムのメモリ管理を最適化することを目的としていました。具体的には、persistentalloc() という新しいヘルパー関数を導入しました。この関数は、SysAlloc()(システムコールによるメモリ割り当て)のラッパーとして機能し、特に小さなチャンクのメモリをキャッシュして効率的に割り当てることを意図していました。

主な目的は以下の通りです。

  • シンボルテーブル(symtab)の割り当て: buildfuncs の処理において、シンボルテーブルのウォーク回数を4回から3回に削減し、大規模なバイナリでのビルド時間を10msから7.5msに短縮し、初期ヒープサイズを680KB削減する効果が見込まれていました。
  • 汎用的な永続的データ割り当て: 型情報(type info)、itab(インターフェーステーブル)の割り当てにも利用できるとされていました。
  • GC(ガベージコレクション)の最適化: GC内の同様の割り当てパターンを persistentalloc() に置き換える可能性が示唆されていました。
  • FixAllocの改善: FixAlloc が128KB単位でメモリを割り当てるという「積極的すぎる」挙動を改善するためにも利用できるとされていました。

persistentalloc() は、割り当てられたメモリが解放されない(永続的である)ことを前提としており、GCの対象外となるデータに適していました。

変更の背景

CL 9805043 で導入された persistentalloc() 関数は、パフォーマンスの向上とメモリ使用量の削減を目指したものでしたが、このコミットのログにあるように「multiple failures on amd64」(amd64アーキテクチャでの複数の障害)が発生したため、その変更を元に戻すことが決定されました。

具体的な障害の内容はこのコミットメッセージからは読み取れませんが、ランタイムのメモリ割り当てのような低レベルな変更は、特定のアーキテクチャや環境において予期せぬクラッシュ、メモリリーク、データ破損などの深刻な問題を引き起こす可能性があります。特に amd64 はGoが広く利用される主要なアーキテクチャであるため、そこで安定性の問題が発生したことは、その変更を即座にロールバックする十分な理由となります。この決定は、パフォーマンス最適化よりもランタイムの安定性と信頼性を優先するというGo開発チームの姿勢を示しています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGoランタイムおよびメモリ管理に関する知識が必要です。

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルなシステムです。これには、スケジューラ(ゴルーチンの管理)、ガベージコレクタ(GC)、メモリ割り当て、システムコールインターフェースなどが含まれます。Goプログラムは、OS上で直接実行されるのではなく、このランタイム上で動作します。

  • SysAlloc(): Goランタイムがオペレーティングシステムから直接メモリを要求するために使用する関数です。これは通常、大きなメモリ領域をOSから取得する際に利用されます。persistentalloc() は、この SysAlloc() を基盤として、より小さなチャンクを効率的に管理しようとしたものです。

  • persistentalloc() (元コミットで導入された機能): 元コミットで導入された、永続的な(GCの対象とならない)小さなメモリチャンクを効率的に割り当てるためのヘルパー関数です。SysAlloc() で取得した大きなメモリブロックをキャッシュとして利用し、そこから小さな要求に応えることで、システムコール頻度を減らし、メモリの断片化を抑えることを目指していました。主に、プログラムの実行中に内容が変化しない、あるいはGCのオーバーヘッドを避けたいデータ(例: シンボルテーブル、型情報)のために設計されました。

  • シンボルテーブル (symtab): Goバイナリに含まれる重要なデータ構造の一つです。関数名、変数名、ファイル名、行番号などのシンボル情報が格納されており、デバッグ、プロファイリング、スタックトレースの生成、リフレクションなどのランタイム機能に不可欠です。Goのバイナリサイズに大きく影響する要素でもあります。

  • mallocgc(): Goランタイムの主要なメモリ割り当て関数です。Goのガベージコレクタによって管理されるヒープメモリからメモリを割り当てます。割り当てられたメモリはGCの対象となり、不要になった時点で自動的に解放されます。

  • FlagNoPointers: mallocgc() に渡されるフラグの一つです。このフラグが設定されている場合、割り当てられるメモリ領域にはポインタが含まれないことをGCに伝えます。これにより、GCはこの領域をスキャンする必要がなくなり、GCのオーバーヘッドを削減できます。シンボルテーブルや一部の型情報など、ポインタを含まないデータ構造の割り当てに適しています。

  • hugestring: このコミットで symtab.c に導入された新しいメカニズムです。persistentalloc() の代替として、シンボルテーブル内の文字列(関数名、ファイル名など)を効率的に管理するために使用されます。これは、すべての文字列を一つの大きな連続したメモリブロック(hugestring)に格納し、個々の文字列はそのブロック内のスライスとして表現するアプローチです。これにより、文字列ごとの個別のメモリ割り当てを減らし、メモリの局所性を高めることができます。

  • amd64: x86-64アーキテクチャのことで、現代のほとんどのデスクトップPCやサーバーで使用されている64ビットプロセッサアーキテクチャです。Goはクロスコンパイルが可能ですが、特定のアーキテクチャに依存する低レベルなランタイムの変更は、そのアーキテクチャ固有のバグを引き起こす可能性があります。

技術的詳細

このコミットは、persistentalloc() の導入を取り消し、その影響を受けた箇所を元の状態に戻すか、代替のメカニズムを導入しています。

  1. src/pkg/runtime/malloc.goc および src/pkg/runtime/malloc.h から persistentalloc() の削除:

    • malloc.goc から persistentalloc() 関数の実装全体が削除されました。これには、persistent 構造体(ロック、ポインタ、エンドポインタを含む)、PersistentAllocChunk および PersistentAllocMaxBlock 定義、そして実際のメモリ割り当てロジックが含まれます。
    • malloc.h から runtime·persistentalloc の関数宣言が削除されました。
    • これにより、persistentalloc() を利用したメモリ割り当てはGoランタイムから完全に排除されました。
  2. src/pkg/runtime/symtab.c の変更: このファイルはシンボルテーブルの構築に関連しており、元コミットで persistentalloc() が利用されていました。このコミットでは、persistentalloc() の使用を中止し、代わりに mallocgc() と新しい hugestring メカニズムを導入しています。

    • func および fname 配列の割り当て: 元コミットでは funcfname の配列を runtime·persistentalloc で割り当てていましたが、このコミットでは runtime·mallocgc に戻されました。

      // 変更前 (元コミット)
      // func = runtime·persistentalloc((nfunc+1)*sizeof func[0], 0);
      // fname = runtime·persistentalloc(nfname*sizeof fname[0], 0);
      
      // 変更後 (このコミット)
      func = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1);
      fname = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);
      

      ここで注目すべきは、mallocgcFlagNoPointers フラグが渡されている点です。これは、これらの配列がポインタを含まない(あるいは、含まれるポインタがGCによってスキャンされる必要がない)ことをGCに伝え、GCの効率を向上させます。

    • hugestring メカニズムの導入: シンボルテーブル内の文字列(関数名、ファイル名など)の管理方法が大きく変更されました。元コミットではこれらの文字列も persistentalloc で個別に割り当てていましたが、このコミットでは hugestring という単一の大きなメモリブロックにすべての文字列を格納する方式が導入されました。

      static String hugestring;
      static int32 hugestring_len;
      

      gostringn 関数が変更され、文字列を hugestring に追加するようになりました。

      // 変更前 (元コミット)
      // s.str = runtime·persistentalloc(l, 1);
      // runtime·memmove(s.str, p, l);
      
      // 変更後 (このコミット)
      if(hugestring.str == nil) {
          hugestring_len += l;
          return runtime·emptystring;
      }
      s.str = hugestring.str + hugestring.len;
      hugestring.len += s.len;
      runtime·memmove(s.str, p, l);
      

      この変更により、gostringn は2つのパスで動作します。

      1. パス1 (hugestring.str == nil の場合): hugestring の総長 (hugestring_len) を計算します。この時点では実際のメモリ割り当ては行いません。
      2. パス2 (hugestring.str != nil の場合): hugestring_len で計算されたサイズに基づいて hugestring のメモリを mallocgc で一度に割り当て、その中にすべての文字列データをコピーしていきます。
      // buildfuncs 関数内の変更
      // ...
      walksymtab(dosrcline);  // pass 1: determine hugestring_len
      hugestring.str = runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0);
      hugestring.len = 0;
      walksymtab(dosrcline);  // pass 2: fill and use hugestring
      // ...
      if(hugestring.len != hugestring_len)
          runtime·throw("buildfunc: problem in initialization procedure");
      

      この二段階の割り当て戦略により、文字列ごとに個別の SysAllocpersistentalloc を呼び出すオーバーヘッドを回避し、メモリの局所性を高めることができます。また、FlagNoPointers を使用することで、GCのスキャン対象から外すことができ、パフォーマンスに寄与します。

このコミットは、persistentalloc() の導入が引き起こした安定性の問題を解決するために、その機能を完全に削除し、影響を受けたシンボルテーブルのメモリ割り当てロジックを、mallocgc() と新しい hugestring メカニズムを用いた、より堅牢で効率的な方法に置き換えたものです。

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

src/pkg/runtime/malloc.goc

  • persistent 構造体、PersistentAllocChunkPersistentAllocMaxBlock の定義が削除されました。
  • runtime·persistentalloc 関数の実装全体(約47行)が削除されました。

src/pkg/runtime/malloc.h

  • void* runtime·persistentalloc(uintptr size, uintptr align); の関数宣言が削除されました。

src/pkg/runtime/symtab.c

  • static String hugestring;static int32 hugestring_len; が追加されました。
  • gostringn 関数内で、runtime·persistentalloc の呼び出しが削除され、hugestring を利用した文字列のコピーロジックに置き換えられました。
  • buildfuncs 関数内で、funcfname の割り当てが runtime·persistentalloc から runtime·mallocgc (FlagNoPointers 付き) に変更されました。
  • buildfuncs 関数内で、walksymtab(dosrcline) が2回呼び出されるようになり、1回目で hugestring_len を計算し、2回目で実際に hugestring に文字列を格納する二段階の処理が導入されました。
  • dosrcline 関数内に if(hugestring.str == nil) のチェックが追加され、hugestring の初期化状態に応じて動作が分岐するようになりました。

コアとなるコードの解説

src/pkg/runtime/malloc.goc および src/pkg/runtime/malloc.h の変更

これらのファイルからの persistentalloc の削除は、この機能がGoランタイムのコアメモリ管理から完全に撤回されたことを意味します。これは、amd64 での障害が、この新しい割り当てメカニズムの根本的な設計または実装上の問題に起因すると判断されたためと考えられます。ランタイムの安定性を最優先するGoの方針に沿った変更です。

src/pkg/runtime/symtab.c の変更

このファイルでの変更は、persistentalloc の代替手段を確立することが目的です。

  • hugestring の導入と gostringn の変更: シンボルテーブル内の文字列は、バイナリの実行中に頻繁にアクセスされるが、変更されることはほとんどない永続的なデータです。元々 persistentalloc はこのような用途を想定していましたが、問題が発生したため、より制御された方法でメモリを管理する必要があります。 hugestring は、すべての文字列を単一の大きな連続したメモリブロックにまとめることで、メモリの断片化を減らし、キャッシュ効率を高めます。gostringn 関数は、この hugestring の現在の位置に新しい文字列をコピーし、hugestring の長さを更新します。これにより、個々の文字列割り当てのオーバーヘッドがなくなります。

  • buildfuncs における二段階の dosrcline 呼び出し: これは hugestring メカニズムの肝となる部分です。

    1. 最初の walksymtab(dosrcline) パスでは、hugestring.strnil であるため、gostringn は文字列を実際にコピーせず、単にすべての文字列の合計長 (hugestring_len) を計算します。
    2. hugestring.str = runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0); で、計算された合計長に基づいて、hugestring 用のメモリが一度に割り当てられます。FlagNoPointers は、このメモリブロックがポインタを含まないためGCスキャンが不要であることを示します。
    3. 二度目の walksymtab(dosrcline) パスでは、hugestring.str が割り当てられているため、gostringn は実際に文字列データを hugestring にコピーしていきます。 この二段階アプローチにより、必要なメモリ量を正確に把握した上で一度に割り当てることができ、効率的なメモリ利用とパフォーマンスを実現します。
  • func および fname 配列への mallocgc(..., FlagNoPointers, ...) の適用: funcfname の配列は、関数情報やファイル名情報を格納するもので、これらもまたポインタを含まないか、含まれてもGCが特別に処理する必要がないデータです。persistentalloc の代わりに mallocgc を使用しつつ FlagNoPointers を指定することで、GCのオーバーヘッドを最小限に抑えながら、Goの標準的なメモリ管理フレームワークに統合されます。

これらの変更は、persistentalloc の問題を回避しつつ、シンボルテーブル関連のメモリ割り当てにおいて、パフォーマンスとメモリ効率を維持するための代替策として機能しています。

関連リンク

参考にした情報源リンク

  • Goのメモリ管理に関する一般的な情報:
  • Goのシンボルテーブルに関する情報:
    • Goのバイナリ構造に関する記事やドキュメント(特定の公式ドキュメントは少ないですが、Goのデバッグやプロファイリングに関する記事で言及されることが多いです)
  • amd64 アーキテクチャに関する一般的な情報。
  • Goのコミット履歴とCL (Change List) の概念。