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

[インデックス 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 の実装には、以下の問題がありました。

  1. 計算の精度とオーバーフローの可能性: rate * runtime·tickspersecond() / (1000*1000*1000) という計算式は、rateruntime·tickspersecond() の積が非常に大きくなる可能性があり、int64 の範囲を超えてオーバーフローする危険性がありました。
  2. 低速CPUでのプロファイリング無効化: 特に1GHz未満の低速なCPUを搭載したマシンでは、runtime·tickspersecond() の値が小さくなるため、rate=1(つまり1ナノ秒ごとにサンプリング)を設定しても、上記の計算結果が 0 になってしまうことがありました。プロファイリングレートが 0 に設定されると、プロファイリングは無効化されるため、これは意図しない挙動でした。ユーザーは最も詳細なプロファイリングを期待して rate=1 を設定しているにもかかわらず、実際にはプロファイリングが行われないという問題が発生していました。
  3. コーナーケースの考慮不足: コミットメッセージにあるように、「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));

この計算にはいくつかの問題がありました。

  1. 整数オーバーフローの可能性: rateruntime·tickspersecond() はどちらも int64 型であり、これらの積 rate * runtime·tickspersecond()int64 の最大値を超えると、オーバーフローが発生し、不正な結果が runtime·blockprofilerate に格納される可能性がありました。
  2. 低速CPUでの rate=1 の問題: runtime·tickspersecond() はCPUの速度に依存します。低速なCPU(例えば1GHz未満)では、この値が小さくなります。rate1 の場合、1 * runtime·tickspersecond() の結果が 1000*1000*1000 (10億) よりも小さくなることがありました。この場合、整数除算 ... / (1000*1000*1000) の結果は 0 になってしまいます。runtime·blockprofilerate0 に設定されると、ブロックプロファイリングは無効になります。これは、ユーザーが最も詳細なプロファイリングを意図して rate=1 を設定したにもかかわらず、実際にはプロファイリングが行われないというバグでした。

このコミットによる修正は、これらの問題を解決するために以下の変更を導入しました。

  1. float64 を使用したオーバーフロー防止: rateruntime·tickspersecond() の積を計算する際に、一時的に float64 型にキャストすることで、中間計算での整数オーバーフローを防ぎます。これにより、より大きな数値範囲での計算が可能になります。 r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);
  2. rate <= 0 の明示的なハンドリング: rate0 以下の場合、プロファイリングを無効にするために r = 0 を明示的に設定します。これは以前のコードでも結果的に 0 になる可能性がありましたが、より意図が明確になりました。
  3. 計算結果が 0 の場合の補正: float64 を使用した計算後、結果 r0 になった場合、それを 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);
  1. int64 r;:計算結果を一時的に保持するためのローカル変数 r が導入されました。
  2. if(rate <= 0)
    • rate0 以下の場合、プロファイリングを無効にするために r0 に設定します。これは、ユーザーがプロファイリングを明示的に無効にしたい場合の正しい挙動です。
  3. else { ... }
    • rate が正の値の場合、プロファイリングを有効にするための計算が行われます。
    • r = (float64)rate*runtime·tickspersecond()/(1000*1000*1000);
      • ratefloat64 にキャストしてから乗算を行うことで、rate * runtime·tickspersecond() の中間結果が int64 の範囲を超えてオーバーフローするのを防ぎます。これにより、より正確な計算が可能になります。
      • 1000*1000*1000 は1秒あたりのナノ秒数(10^9)です。
    • if(r == 0)
      • float64 で計算された r0 になった場合(これは主に低速なCPUで rate=1 の場合に発生する問題でした)、r1 に設定します。これにより、プロファイリングが意図せず無効になることを防ぎ、最小限のサンプリングレート(最も詳細なプロファイリング)が保証されます。
  4. runtime·atomicstore64((uint64*)&runtime·blockprofilerate, r);
    • 最終的に計算された r の値を、runtime·blockprofilerate にアトミックに格納します。アトミック操作は、複数のゴルーチンからの同時アクセスに対して安全性を保証します。

これらの変更により、SetBlockProfileRate 関数は、オーバーフローの危険性を回避し、低速なCPU環境でも rate=1 が正しく機能するように改善され、より堅牢なブロックプロファイリング機能を提供できるようになりました。

関連リンク

参考にした情報源リンク