[インデックス 17247] ファイルの概要
このコミットは、Goランタイムのブロックプロファイリング機能における SetBlockProfileRate 関数のバグを修正するものです。特に、CPU速度が遅いマシンで SetBlockProfileRate(1) を設定するとプロファイリングが無効になってしまう問題や、計算におけるコーナーケースの欠落に対処しています。
コミット
commit 3bd0b0a80dd78bacf814cbe51e427dac0fd231c3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Aug 15 00:20:36 2013 +0400
runtime: fix SetBlockProfileRate
It doughtily misses all possible corner cases.
In particular on machines with <1GHz processors,
SetBlockProfileRate(1) disables profiling.
Fixes #6114.
R=golang-dev, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/12936043
---
src/pkg/runtime/mprof.goc | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/pkg/runtime/mprof.goc b/src/pkg/runtime/mprof.goc
index 6e51ef3eb1..473e6e11cf 100644
--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -295,7 +295,17 @@ int64 runtime·blockprofilerate; // in CPU ticks
void
runtime·SetBlockProfileRate(intgo rate)
{
- runtime·atomicstore64((uint64*)&runtime·blockprofilerate, rate * runtime·tickspersecond() / (1000*1000*1000));
+ int64 r;
+
+ if(rate <= 0)
+ r = 0; // disable profiling
+ else {
+ // convert ns to cycles, use float64 to prevent overflow during multiplication
+ r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);
+ if(r == 0)
+ r = 1;
+ }
+ runtime·atomicstore64((uint64*)&runtime·blockprofilerate, r);
}
void
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3bd0b0a80dd78bacf814cbe51e427dac0fd231c3
元コミット内容
このコミットは、Goランタイムの SetBlockProfileRate 関数における複数の問題を修正することを目的としています。特に、この関数が「考えられないほどのコーナーケースを見逃している」と指摘されており、その結果として、1GHz未満のプロセッサを搭載したマシンで SetBlockProfileRate(1) を呼び出すと、ブロックプロファイリングが意図せず無効になってしまうという具体的なバグ(Issue #6114)を修正します。
変更の背景
Goのブロックプロファイラは、ゴルーチンが同期プリミティブでブロックされた時間を追跡するために使用されます。runtime.SetBlockProfileRate(rate int) 関数は、このブロックプロファイラのサンプリングレートを制御します。rate の値はナノ秒単位で、平均して rate ナノ秒ごとに1つのブロッキングイベントをサンプリングすることを目指します。
このコミットが行われる前の SetBlockProfileRate の実装には、以下の問題がありました。
- 計算の精度とオーバーフローの可能性:
rate * runtime·tickspersecond() / (1000*1000*1000)という計算式は、rateとruntime·tickspersecond()の積が非常に大きくなる可能性があり、int64の範囲を超えてオーバーフローする危険性がありました。 - 低速CPUでのプロファイリング無効化: 特に1GHz未満の低速なCPUを搭載したマシンでは、
runtime·tickspersecond()の値が小さくなるため、rate=1(つまり1ナノ秒ごとにサンプリング)を設定しても、上記の計算結果が0になってしまうことがありました。プロファイリングレートが0に設定されると、プロファイリングは無効化されるため、これは意図しない挙動でした。ユーザーは最も詳細なプロファイリングを期待してrate=1を設定しているにもかかわらず、実際にはプロファイリングが行われないという問題が発生していました。 - コーナーケースの考慮不足: コミットメッセージにあるように、「doughtily misses all possible corner cases」(考えられないほどのコーナーケースを見逃している)という表現が示す通り、
rateの値が0以下の場合や、計算結果が0になる場合など、様々なエッジケースに対する適切なハンドリングが欠けていました。
これらの問題は、Goアプリケーションのパフォーマンス分析において、ブロックプロファイリングが正確に機能しないという深刻な影響を及ぼしていました。
前提知識の解説
Goのプロファイリング
Goには、プログラムのパフォーマンス特性を分析するための組み込みプロファイリングツールが用意されています。これには、CPUプロファイリング、メモリプロファイリング、ゴルーチンプロファイリング、そしてブロックプロファイリングなどが含まれます。
ブロックプロファイリング (Block Profiling)
ブロックプロファイリングは、ゴルーチンが同期プリミティブ(ミューテックス、チャネル操作など)でブロックされて待機している時間を測定します。これにより、プログラム内の並行処理のボトルネックや、不要なロック競合などを特定するのに役立ちます。
runtime.SetBlockProfileRate
この関数は、ブロックプロファイリングのサンプリングレートを設定します。引数 rate はナノ秒単位で、プロファイラは平均して rate ナノ秒ごとに1つのブロッキングイベントをサンプリングしようとします。
rate = 1: ほぼすべてのブロッキングイベントをサンプリングします。最も詳細なプロファイルが得られますが、オーバーヘッドも大きくなります。rate <= 0: ブロックプロファイリングを無効にします。rate > 1: サンプリング間隔が長くなり、オーバーヘッドは減少しますが、詳細度は低下します。
runtime·tickspersecond() (内部関数)
これはGoランタイムの内部関数で、CPUの1秒あたりのティック数(またはサイクル数)を表します。Goのプロファイラは、cputicks() のような高解像度の時間測定にCPUティックを使用し、これをナノ秒などの時間単位に変換するために tickspersecond の値を利用します。この変換は、ブロックされた時間の正確な報告に不可欠です。
CPUティックとナノ秒
- CPUティック: CPUが実行する最小の時間単位であり、CPUのクロックサイクルに対応します。CPUの周波数に依存します。
- ナノ秒: 10億分の1秒。Goのプロファイリングレートはナノ秒単位で指定されます。
SetBlockProfileRate の内部では、ユーザーが指定したナノ秒単位の rate を、内部で利用するCPUティック単位のレートに変換する必要があります。この変換が rate * runtime·tickspersecond() / (1000*1000*1000) という計算で行われていました。ここで 1000*1000*1000 は1秒あたりのナノ秒数(10^9)です。
技術的詳細
修正前のコードでは、runtime·SetBlockProfileRate 関数内で、ユーザーが指定した rate (ナノ秒) をCPUティック単位のレートに変換するために以下の計算を行っていました。
runtime·atomicstore64((uint64*)&runtime·blockprofilerate, rate * runtime·tickspersecond() / (1000*1000*1000));
この計算にはいくつかの問題がありました。
- 整数オーバーフローの可能性:
rateとruntime·tickspersecond()はどちらもint64型であり、これらの積rate * runtime·tickspersecond()がint64の最大値を超えると、オーバーフローが発生し、不正な結果がruntime·blockprofilerateに格納される可能性がありました。 - 低速CPUでの
rate=1の問題:runtime·tickspersecond()はCPUの速度に依存します。低速なCPU(例えば1GHz未満)では、この値が小さくなります。rateが1の場合、1 * runtime·tickspersecond()の結果が1000*1000*1000(10億) よりも小さくなることがありました。この場合、整数除算... / (1000*1000*1000)の結果は0になってしまいます。runtime·blockprofilerateが0に設定されると、ブロックプロファイリングは無効になります。これは、ユーザーが最も詳細なプロファイリングを意図してrate=1を設定したにもかかわらず、実際にはプロファイリングが行われないというバグでした。
このコミットによる修正は、これらの問題を解決するために以下の変更を導入しました。
float64を使用したオーバーフロー防止:rateとruntime·tickspersecond()の積を計算する際に、一時的にfloat64型にキャストすることで、中間計算での整数オーバーフローを防ぎます。これにより、より大きな数値範囲での計算が可能になります。r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);rate <= 0の明示的なハンドリング:rateが0以下の場合、プロファイリングを無効にするためにr = 0を明示的に設定します。これは以前のコードでも結果的に0になる可能性がありましたが、より意図が明確になりました。- 計算結果が
0の場合の補正:float64を使用した計算後、結果rが0になった場合、それを1に補正します。if(r == 0)r = 1;このr = 1の補正は、特に低速なCPUでrate=1を設定した際に、計算結果が0になってプロファイリングが無効になる問題を解決します。1に設定することで、最小限のサンプリングレート(つまり、可能な限り詳細なプロファイリング)が保証されます。
これらの変更により、SetBlockProfileRate はより堅牢になり、様々なCPU速度や rate の値に対して意図通りに機能するようになりました。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -295,7 +295,17 @@ int64 runtime·blockprofilerate; // in CPU ticks
void
runtime·SetBlockProfileRate(intgo rate)
{
- runtime·atomicstore64((uint64*)&runtime·blockprofilerate, rate * runtime·tickspersecond() / (1000*1000*1000));
+ int64 r;
+
+ if(rate <= 0)
+ r = 0; // disable profiling
+ else {
+ // convert ns to cycles, use float64 to prevent overflow during multiplication
+ r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);
+ if(r == 0)
+ r = 1;
+ }
+ runtime·atomicstore64((uint64*)&runtime·blockprofilerate, r);
}
void
コアとなるコードの解説
変更は src/pkg/runtime/mprof.goc ファイル内の runtime·SetBlockProfileRate 関数に集中しています。
変更前:
runtime·atomicstore64((uint64*)&runtime·blockprofilerate, rate * runtime·tickspersecond() / (1000*1000*1000));
この一行で、rate (ナノ秒) をCPUティック単位のレートに変換し、runtime·blockprofilerate にアトミックに格納していました。前述の通り、この計算はオーバーフローの可能性と、結果が 0 になることによるプロファイリング無効化の問題を抱えていました。
変更後:
int64 r; // 新しいレートを一時的に保持する変数
if(rate <= 0)
r = 0; // disable profiling
else {
// convert ns to cycles, use float64 to prevent overflow during multiplication
r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);
if(r == 0)
r = 1;
}
runtime·atomicstore64((uint64*)&runtime·blockprofilerate, r);
int64 r;:計算結果を一時的に保持するためのローカル変数rが導入されました。if(rate <= 0):rateが0以下の場合、プロファイリングを無効にするためにrを0に設定します。これは、ユーザーがプロファイリングを明示的に無効にしたい場合の正しい挙動です。
else { ... }:rateが正の値の場合、プロファイリングを有効にするための計算が行われます。r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);:rateをfloat64にキャストしてから乗算を行うことで、rate * runtime·tickspersecond()の中間結果がint64の範囲を超えてオーバーフローするのを防ぎます。これにより、より正確な計算が可能になります。1000*1000*1000は1秒あたりのナノ秒数(10^9)です。
if(r == 0):float64で計算されたrが0になった場合(これは主に低速なCPUでrate=1の場合に発生する問題でした)、rを1に設定します。これにより、プロファイリングが意図せず無効になることを防ぎ、最小限のサンプリングレート(最も詳細なプロファイリング)が保証されます。
runtime·atomicstore64((uint64*)&runtime·blockprofilerate, r);:- 最終的に計算された
rの値を、runtime·blockprofilerateにアトミックに格納します。アトミック操作は、複数のゴルーチンからの同時アクセスに対して安全性を保証します。
- 最終的に計算された
これらの変更により、SetBlockProfileRate 関数は、オーバーフローの危険性を回避し、低速なCPU環境でも rate=1 が正しく機能するように改善され、より堅牢なブロックプロファイリング機能を提供できるようになりました。
関連リンク
- Go Issue #6114: https://code.google.com/p/go/issues/detail?id=6114 (現在は
https://golang.org/issue/6114にリダイレクトされます) - Go CL (Change List) 12936043: https://golang.org/cl/12936043
参考にした情報源リンク
- Go Documentation:
runtime.SetBlockProfileRate(Go 1.20): https://pkg.go.dev/runtime#SetBlockProfileRate - Go Documentation:
runtime.SetBlockProfileRate(Go 1.21): https://pkg.go.dev/runtime@go1.21.0#SetBlockProfileRate - GitHub:
src/runtime/mprof.go(current): https://github.com/golang/go/blob/master/src/runtime/mprof.go - GitHub:
src/runtime/proc.go(current, forcyclesPerSecond): https://github.com/golang/go/blob/master/src/runtime/proc.go - Go Wiki: Profiling Go Programs: https://go.dev/doc/diagnostics#profiling