[インデックス 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
の呼び出しがヒープアロケーションを伴わなくなったことを意味します。ただし、BenchmarkDeferMany
のbytes
がわずかに増加しているのは、新しいアロケーション戦略によるオーバーヘッドの可能性がありますが、全体的なパフォーマンス改善に寄与しています。
前提知識の解説
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
構造体を割り当てるための新しい内部関数です。この関数は、以下のロジックで動作します。
- 現在のチャンクへの適合性チェック: まず、現在のゴルーチンの
dchunk
に、新しいDefer
構造体を格納するのに十分なスペースがあるかを確認します。 - チャンク内アロケーション: 十分なスペースがあれば、
dchunk
内のoff
をインクリメントして、そのチャンク内にDefer
構造体を割り当てます。これにより、個別のmalloc
呼び出しが不要になります。 - 新しいチャンクの割り当て: 現在のチャンクにスペースがない場合、または新しい
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の場合に、このDefer
がfreedefer
で解放されるべきかどうかを示すフラグ。
runtime·deferproc
の変更
runtime·deferproc
関数は、コンパイラがdefer
ステートメントを処理するために呼び出す関数です。この関数は、Defer
構造体を個別にruntime·malloc
で割り当てる代わりに、新しく導入されたnewdefer
関数を呼び出すように変更されました。
runtime·deferreturn
とrundefer
の変更
これらの関数は、defer
された関数が実行された後にDefer
構造体を処理する役割を担います。
g->defer = d->link;
の代わりにpopdefer();
が呼び出されるようになりました。runtime·free(d);
の代わりにfreedefer(d);
が呼び出されるようになりました。
runtime·panic
の変更
panic
処理中にもdefer
が実行されるため、panic
関数もpopdefer()
とfreedefer(d)
を使用するように変更されました。また、panic
からのリカバリ時に、リカバリされたフレームの情報(argp
とpc
)をg->sigcode0
とg->sigcode1
に格納するように変更され、Defer
構造体をg->defer
リストに戻す必要がなくなりました。
runtime·cgocall
とruntime·cgocallbackg
の変更
cgocall.c
内のruntime·cgocall
とruntime·cgocallbackg
関数では、Defer
構造体のnofree
フィールドがspecial
フィールドにリネームされ、そのロジックが新しいspecial
フラグのセマンティクスに合わせて調整されました。
コアとなるコードの変更箇所
src/pkg/runtime/cgocall.c
d.nofree
がd.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->sigcode0
とgp->sigcode1
からリカバリ情報を取得するように変更。
src/pkg/runtime/runtime.h
DeferChunk
構造体の定義を追加。G
構造体にdchunk
とdchunknext
フィールドを追加。Defer
構造体のnofree
フィールドをspecial
とfree
フィールドに置き換え。
src/pkg/runtime/runtime_test.go
BenchmarkDefer
,BenchmarkDefer10
,BenchmarkDeferMany
という新しいベンチマーク関数が追加されました。これらは、defer
のパフォーマンスを測定するために使用されます。defer1()
とdefer2()
というヘルパー関数が追加され、それぞれ単一のdefer
と複数のdefer
のシナリオをシミュレートします。
コアとなるコードの解説
このコミットの核心は、defer
構造体のメモリ管理を、個別のヒープアロケーションから、ゴルーチンごとに管理されるチャンクベースのアロケーションに移行した点にあります。
-
DeferChunk
とG
構造体: 各ゴルーチン(G
)は、dchunk
とdchunknext
という2つのDeferChunk
ポインタを持つようになりました。dchunk
は現在アクティブなチャンクを指し、dchunknext
は次に使用可能なチャンク(以前使用されて空になったチャンクなど)を指します。これにより、チャンクの再利用が可能になり、malloc
呼び出しの頻度が削減されます。 -
newdefer
の役割:newdefer
関数は、Defer
構造体を割り当てる際の中心的なロジックを担います。- ほとんどの小さな
defer
は、既存のDeferChunk
内に連続して割り当てられます。これにより、多数のdefer
が呼び出されても、ヒープアロケーションはチャンク単位でしか発生せず、個々のdefer
呼び出しではアロケーションが発生しなくなります。これがベンチマークでallocs
が100%削減された主な理由です。 DeferChunkSize
を超えるような大きなdefer
(例えば、非常に大きな引数を持つ場合)は、引き続き個別にヒープに割り当てられます。これらはspecial
フラグでマークされ、freedefer
で個別に解放されます。
- ほとんどの小さな
-
popdefer
とfreedefer
の役割: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は公開リポジトリでは見つかりませんでした。内部的なトラッカーの可能性があります。)