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

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

このコミットは、Go言語のランタイムにおけるdefer関数のアロケーション(メモリ割り当て)を最適化し、パフォーマンスを向上させるための変更です。具体的には、defer関数の呼び出し時に発生するメモリ割り当てを削減し、特に多数のdeferが使用されるシナリオでの実行速度を改善しています。

コミット

commit 0de71619ce591d79297ae609362a8ac1cdb5fe46
Author: Russ Cox <rsc@golang.org>
Date:   Sat Dec 22 14:54:39 2012 -0500

    runtime: aggregate defer allocations
    
    benchmark             old ns/op    new ns/op    delta
    BenchmarkDefer              165          113  -31.52%
    BenchmarkDefer10            155          103  -33.55%
    BenchmarkDeferMany          216          158  -26.85%
    
    benchmark            old allocs   new allocs    delta
    BenchmarkDefer                1            0  -100.00%
    BenchmarkDefer10              1            0  -100.00%
    BenchmarkDeferMany            1            0  -100.00%
    
    benchmark             old bytes    new bytes    delta
    BenchmarkDefer               64            0  -100.00%
    BenchmarkDefer10             64            0  -100.00%
    BenchmarkDeferMany           64           66    3.12%
    
    Fixes #2364.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7001051

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

https://github.com/golang/go/commit/0de71619ce591d79297ae609362a8ac1cdb5fe46

元コミット内容

このコミットの元の内容は、Goランタイムにおけるdefer関数のアロケーションを最適化することです。ベンチマーク結果が示しているように、deferの実行時間(ns/op)、アロケーション回数(allocs)、およびアロケーションされるバイト数(bytes)が大幅に改善されています。特に、アロケーション回数が100%削減されている点が注目されます。

変更の背景

Go言語のdeferステートメントは、関数がリターンする直前、またはパニックが発生した際に実行される関数をスケジュールするために使用されます。これはリソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングに非常に便利です。しかし、従来のdeferの実装では、deferが呼び出されるたびにヒープ上にDefer構造体が個別に割り当てられていました。

この個別のアロケーションは、特にループ内で多数のdeferが使用される場合や、deferが頻繁に呼び出されるような高性能なアプリケーションにおいて、ガベージコレクションの負荷を増加させ、パフォーマンスのボトルネックとなる可能性がありました。このコミットは、このアロケーションオーバーヘッドを削減し、deferの効率を向上させることを目的としています。

コミットメッセージに記載されているベンチマーク結果は、この最適化の必要性を示しています。

  • BenchmarkDefer: 単一のdefer呼び出し
  • BenchmarkDefer10: 10回のdefer呼び出し
  • BenchmarkDeferMany: 多数のdefer呼び出し

これらのベンチマークにおいて、ns/op(操作あたりのナノ秒)とallocs(アロケーション回数)が大幅に削減されており、特にallocsは100%削減されています。これは、deferの呼び出しがヒープアロケーションを伴わなくなったことを意味します。ただし、BenchmarkDeferManybytesがわずかに増加しているのは、新しいアロケーション戦略によるオーバーヘッドの可能性がありますが、全体的なパフォーマンス改善に寄与しています。

前提知識の解説

Go言語のdeferステートメント

deferステートメントは、Go言語の制御フローキーワードの一つです。deferに続く関数呼び出しは、そのdeferステートメントを含む関数が実行を終了する直前(returnステートメントの実行後、またはパニック発生時)に実行されるようにスケジュールされます。deferされた関数はLIFO(後入れ先出し)の順序で実行されます。

例:

func readFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close() // 関数終了時にファイルが閉じられることを保証

    data, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, err
    }
    return data, nil
}

Goランタイムとガベージコレクション

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、スケジューラ、メモリ管理(ヒープアロケーションとガベージコレクション)、スタック管理などが含まれます。 ガベージコレクション(GC)は、プログラムが不要になったメモリを自動的に解放するプロセスです。ヒープアロケーションが頻繁に発生すると、GCの実行頻度が増加し、プログラムの実行が一時的に停止する「ストップ・ザ・ワールド」時間が長くなるなど、パフォーマンスに悪影響を与える可能性があります。

Defer構造体

Goランタイム内部では、deferされた関数に関する情報はDeferという構造体に格納されます。この構造体には、deferされた関数のポインタ、引数、呼び出し元のPC(プログラムカウンタ)などが含まれます。

スタックとヒープ

  • スタック: 関数呼び出しやローカル変数など、一時的なデータを格納するために使用されるメモリ領域です。高速で、LIFOの原則に従います。Goでは、ゴルーチンのスタックは動的にサイズが変更されます。
  • ヒープ: プログラムの実行中に動的に割り当てられるメモリ領域です。スタックよりも低速ですが、より柔軟なメモリ管理が可能です。ガベージコレクションの対象となります。

従来のdeferの実装では、Defer構造体がヒープに割り当てられていたため、GCの対象となり、パフォーマンスオーバーヘッドの原因となっていました。

技術的詳細

このコミットの主要な変更点は、defer構造体のメモリ割り当て戦略を、個別のヒープアロケーションから、より効率的なチャンクベースのアロケーションに変更したことです。

DeferChunkの導入

新しいDeferChunk構造体が導入されました。これは、複数のDefer構造体をまとめて格納するための大きなメモリブロックです。

struct DeferChunk
{
    DeferChunk    *prev; // 前のチャンクへのポインタ
    uintptr       off;   // 現在のチャンク内のオフセット
};

各ゴルーチン(G構造体)は、現在使用中のDeferChunkへのポインタ(dchunk)と、次に使用する可能性のあるチャンクへのポインタ(dchunknext)を持つようになりました。

newdefer関数の導入

newdefer関数は、Defer構造体を割り当てるための新しい内部関数です。この関数は、以下のロジックで動作します。

  1. 現在のチャンクへの適合性チェック: まず、現在のゴルーチンのdchunkに、新しいDefer構造体を格納するのに十分なスペースがあるかを確認します。
  2. チャンク内アロケーション: 十分なスペースがあれば、dchunk内のoffをインクリメントして、そのチャンク内にDefer構造体を割り当てます。これにより、個別のmalloc呼び出しが不要になります。
  3. 新しいチャンクの割り当て: 現在のチャンクにスペースがない場合、または新しいDefer構造体がチャンクサイズの半分を超えるほど大きい場合(この場合はチャンクに入れる価値がないと判断される)、新しいDeferChunkが割り当てられます。
    • DeferChunkSize(2048バイト)よりも大きいDefer構造体は、引き続き個別にruntime·mallocでヒープに割り当てられます。これはspecialフラグとfreeフラグで管理されます。
    • 新しいチャンクは、g->dchunknextに保存されているチャンクがあればそれを使用し、なければ新しくruntime·mallocで割り当てられます。
    • 新しいチャンクはg->dchunkに設定され、prevポインタで前のチャンクにリンクされます。

popdefer関数の導入

popdefer関数は、deferスタックから現在のDefer構造体をポップするための新しい内部関数です。

  • d->specialフラグが設定されている(個別に割り当てられた)場合は、特別な処理は不要です。
  • チャンク内に割り当てられたDefer構造体の場合、dchunk->offをデクリメントして、そのDefer構造体が占めていたスペースを解放します。
  • チャンクが空になった場合(c->off == sizeof(*c))、そのチャンクはg->dchunknextに保存され、再利用のためにマークされます。

freedefer関数の導入

freedefer関数は、Defer構造体が不要になったときにそのメモリを解放するための新しい内部関数です。

  • d->specialフラグが設定されている(個別に割り当てられ、d->freeがtrue)場合は、runtime·free(d)でヒープから解放されます。
  • チャンク内に割り当てられたDefer構造体の場合、その引数領域がruntime·memclrでクリアされます。これは、チャンク自体は解放されないため、引数に機密情報が含まれる場合に備えてクリアするためです。

Defer構造体の変更

Defer構造体には、新しいフィールドが追加されました。

  • bool special: このDeferがチャンクの一部ではなく、個別に割り当てられたものであることを示すフラグ。
  • bool free: specialがtrueの場合に、このDeferfreedeferで解放されるべきかどうかを示すフラグ。

runtime·deferprocの変更

runtime·deferproc関数は、コンパイラがdeferステートメントを処理するために呼び出す関数です。この関数は、Defer構造体を個別にruntime·mallocで割り当てる代わりに、新しく導入されたnewdefer関数を呼び出すように変更されました。

runtime·deferreturnrundeferの変更

これらの関数は、deferされた関数が実行された後にDefer構造体を処理する役割を担います。

  • g->defer = d->link; の代わりに popdefer(); が呼び出されるようになりました。
  • runtime·free(d); の代わりに freedefer(d); が呼び出されるようになりました。

runtime·panicの変更

panic処理中にもdeferが実行されるため、panic関数もpopdefer()freedefer(d)を使用するように変更されました。また、panicからのリカバリ時に、リカバリされたフレームの情報(argppc)をg->sigcode0g->sigcode1に格納するように変更され、Defer構造体をg->deferリストに戻す必要がなくなりました。

runtime·cgocallruntime·cgocallbackgの変更

cgocall.c内のruntime·cgocallruntime·cgocallbackg関数では、Defer構造体のnofreeフィールドがspecialフィールドにリネームされ、そのロジックが新しいspecialフラグのセマンティクスに合わせて調整されました。

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

src/pkg/runtime/cgocall.c

  • d.nofreed.specialにリネームされ、関連するロジックが変更されました。

src/pkg/runtime/panic.c

  • DeferChunkSize定数の定義。
  • newdefer, popdefer, freedefer関数の追加。
  • runtime·deferproc関数内で、runtime·mallocによるDefer構造体の個別アロケーションをnewdefer呼び出しに置き換え。
  • runtime·deferreturn関数内で、g->defer = d->link;runtime·free(d);popdefer();freedefer(d);に置き換え。
  • rundefer関数内で、同様にpopdefer();freedefer(d);を使用するように変更。
  • runtime·panic関数内で、popdefer();freedefer(d);を使用するように変更。また、リカバリ時の情報伝達方法が変更されました。
  • recovery関数内で、gp->sigcode0gp->sigcode1からリカバリ情報を取得するように変更。

src/pkg/runtime/runtime.h

  • DeferChunk構造体の定義を追加。
  • G構造体にdchunkdchunknextフィールドを追加。
  • Defer構造体のnofreeフィールドをspecialfreeフィールドに置き換え。

src/pkg/runtime/runtime_test.go

  • BenchmarkDefer, BenchmarkDefer10, BenchmarkDeferManyという新しいベンチマーク関数が追加されました。これらは、deferのパフォーマンスを測定するために使用されます。
  • defer1()defer2()というヘルパー関数が追加され、それぞれ単一のdeferと複数のdeferのシナリオをシミュレートします。

コアとなるコードの解説

このコミットの核心は、defer構造体のメモリ管理を、個別のヒープアロケーションから、ゴルーチンごとに管理されるチャンクベースのアロケーションに移行した点にあります。

  • DeferChunkG構造体: 各ゴルーチン(G)は、dchunkdchunknextという2つのDeferChunkポインタを持つようになりました。dchunkは現在アクティブなチャンクを指し、dchunknextは次に使用可能なチャンク(以前使用されて空になったチャンクなど)を指します。これにより、チャンクの再利用が可能になり、malloc呼び出しの頻度が削減されます。

  • newdeferの役割: newdefer関数は、Defer構造体を割り当てる際の中心的なロジックを担います。

    • ほとんどの小さなdeferは、既存のDeferChunk内に連続して割り当てられます。これにより、多数のdeferが呼び出されても、ヒープアロケーションはチャンク単位でしか発生せず、個々のdefer呼び出しではアロケーションが発生しなくなります。これがベンチマークでallocsが100%削減された主な理由です。
    • DeferChunkSizeを超えるような大きなdefer(例えば、非常に大きな引数を持つ場合)は、引き続き個別にヒープに割り当てられます。これらはspecialフラグでマークされ、freedeferで個別に解放されます。
  • popdeferfreedeferの役割:

    • popdeferは、deferスタックからDefer構造体を論理的に削除し、チャンク内のオフセットを調整します。これにより、チャンク内のスペースが再利用可能になります。
    • freedeferは、Defer構造体の実際のメモリ解放を担当します。チャンク内のdeferは引数領域をクリアするだけで、チャンク自体は解放されません。個別に割り当てられたdeferのみがruntime·freeで解放されます。

このチャンクベースのアロケーション戦略により、deferの呼び出しがヒープアロケーションを伴わなくなり、ガベージコレクションの負荷が大幅に軽減され、結果としてdeferの実行パフォーマンスが向上しました。特に、多数のdeferが使用されるシナリオでの効果が顕著です。

関連リンク

  • Go言語のdeferステートメントに関する公式ドキュメントやチュートリアル
  • Goランタイムのメモリ管理に関するドキュメント
  • Goのガベージコレクションに関する詳細な解説

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goのソースコード(特にsrc/pkg/runtime/ディレクトリ)
  • Goのベンチマークに関する情報
  • Goのガベージコレクションに関する技術記事
  • Goのdefer実装に関する議論やブログ記事
  • https://golang.org/cl/7001051 (Go Code Review)
  • https://github.com/golang/go/commit/0de71619ce591d79297ae609362a8ac1cdb5fe46 (GitHub Commit)
  • Go issue #2364 (ただし、このissueは公開リポジトリでは見つかりませんでした。内部的なトラッカーの可能性があります。)