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

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

このコミットは、GoランタイムにおけるCPUプロファイリングの信号損失を防ぐための重要な変更を導入しています。具体的には、ガベージコレクション(GC)、スタックスプリット、スケジューラ、システムコールなど、これまでプロファイリングの対象外となっていた、あるいは正確に計測されていなかった処理からのプロファイリング信号を捕捉し、より正確なプロファイルデータを提供することを目指しています。

コミット

commit cc4e6aad8ec18b4ee7fe0392f30f229ddb979589
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Aug 13 22:12:02 2013 +0400

    runtime: do no lose CPU profiling signals
    Currently we lose lots of profiling signals.
    Most notably, GC is not accounted at all.
    But stack splits, scheduler, syscalls, etc are lost as well.
    This creates seriously misleading profile.
    With this change all profiling signals are accounted.
    Now I see these additional entries that were previously absent:
    161  29.7%  29.7%      164  30.3% syscall.Syscall
     12   2.2%  50.9%       12   2.2% scanblock
     11   2.0%  55.0%       11   2.0% markonly
     10   1.8%  58.9%       10   1.8% sweepspan
      2   0.4%  85.8%        2   0.4% runtime.newstack
    It is still impossible to understand what causes stack splits,
    but at least it's clear how many time is spent on them.
    Update #2197.
    Update #5659.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12179043

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

https://github.com/golang/go/commit/cc4e6aad8ec18b4ee7fe0392f30f229ddb979589

元コミット内容

runtime: do no lose CPU profiling signals
Currently we lose lots of profiling signals.
Most notably, GC is not accounted at all.
But stack splits, scheduler, syscalls, etc are lost as well.
This creates seriously misleading profile.
With this change all profiling signals are accounted.
Now I see these additional entries that were previously absent:
161  29.7%  29.7%      164  30.3% syscall.Syscall
 12   2.2%  50.9%       12   2.2% scanblock
 11   2.0%  55.0%       11   2.0% markonly
 10   1.8%  58.9%       10   1.8% sweepspan
  2   0.4%  85.8%        2   0.4% runtime.newstack
It is still impossible to understand what causes stack splits,
but at least it's clear how many time is spent on them.
Update #2197.
Update #5659.

変更の背景

GoのCPUプロファイリングは、プログラムがCPU時間をどこで消費しているかを特定するための重要なツールです。しかし、このコミット以前は、プロファイリング信号(SIGPROF)が特定の状況下で失われるという問題がありました。特に、ガベージコレクション(GC)の実行中、スタックスプリット(Goルーチンのスタックが拡張される処理)、スケジューラの動作、システムコールなど、ランタイム内部の重要な処理がプロファイルに適切に反映されていませんでした。

これにより、生成されるプロファイルデータは誤解を招く可能性があり、開発者はアプリケーションの真のパフォーマンスボトルネックを特定することが困難でした。例えば、GCが多くのCPU時間を消費しているにもかかわらず、プロファイルにはその情報が全く表示されない、といった状況が発生していました。

このコミットの目的は、これらの失われたプロファイリング信号を捕捉し、GoプログラムのCPU使用率をより正確に反映したプロファイルを提供することです。これにより、開発者はより信頼性の高いプロファイルデータに基づいて最適化を行うことができるようになります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とプロファイリングの仕組みに関する知識が必要です。

  • CPUプロファイリングとSIGPROF: CPUプロファイリングは、一定の間隔(通常はタイマー割り込みによってトリガーされるSIGPROFシグナル)でプログラムの実行状態(特にプログラムカウンタ)をサンプリングし、どの関数がCPU時間を消費しているかを統計的に推定する手法です。SIGPROFはUnix系システムで利用されるシグナルで、プロファイリングのために定期的にプロセスに送信されます。

  • GoルーチンとM/P/Gモデル: Goランタイムは、Goルーチン(G)、論理プロセッサ(P)、OSスレッド(M)という3つの主要なエンティティで構成されるスケジューラモデルを採用しています。

    • G (Goroutine): Goの軽量な並行実行単位。
    • P (Processor): Goルーチンを実行するための論理的なコンテキスト。OSスレッド(M)にアタッチされ、Goルーチンキューを管理します。
    • M (Machine): OSスレッド。Goルーチンを実行したり、システムコールを実行したりします。
  • m->g0m->gsignal: 各OSスレッド(M)には、特別なGoルーチンが2つ関連付けられています。

    • m->g0: ランタイムのスケジューラやGCなどの内部処理を実行するためのシステムスタックを持つGoルーチン。ユーザーコードは実行しません。
    • m->gsignal: シグナルハンドラが実行される際に使用される特別なGoルーチン。シグナルハンドラは通常のGoルーチンのスタックではなく、このgsignalのスタックで実行されます。
  • runtime·gentraceback: Goランタイム内部の関数で、指定されたプログラムカウンタ(PC)とスタックポインタ(SP)からスタックトレースを生成します。プロファイリングにおいては、SIGPROFシグナルが捕捉された時点のPCとSPから、現在実行中の関数のコールスタックを取得するために使用されます。

  • スタックスプリット: Goルーチンのスタックは最初は小さく確保され、必要に応じて動的に拡張されます。この拡張処理をスタックスプリットと呼びます。スタックスプリットはランタイム内部の処理であり、ユーザーコードの実行とは異なるコンテキストで行われます。

  • レース検出器(Race Detector)とasmcgocall: Goのレース検出器は、並行処理におけるデータ競合を検出するためのツールです。asmcgocallは、GoコードからCコード(またはアセンブリコード)を呼び出す際に使用されるランタイム内部のメカニズムです。レース検出器が有効な場合、特定のasmcgocallの呼び出しは、通常のGoルーチンの実行コンテキストとは異なる状態になることがあります。

技術的詳細

このコミットの主要な変更は、src/pkg/runtime/proc.cruntime·sigprof関数と、各アーキテクチャ固有のsignal_*.cファイル(signal_386.c, signal_amd64.c, signal_arm.c)におけるruntime·sighandler関数の変更にあります。

runtime·sigprof関数の変更 (src/pkg/runtime/proc.c)

以前のruntime·sigprof関数では、プロファイリング信号が到着した際に、特定の条件(gp != m->g0 && gp != m->gsignal)を満たさないGoルーチンからの信号は無視されていました。これは、m->g0(ランタイム内部処理)やm->gsignal(シグナルハンドラ)で発生したプロファイリング信号が、通常のGoルーチンのスタックトレースとは異なる性質を持つため、正確なスタックトレースの取得が困難であるという仮定に基づいていた可能性があります。しかし、これによりGCやスケジューラなどの重要なランタイム処理がプロファイルから完全に抜け落ちていました。

このコミットでは、runtime·sigprof関数にtracebackという新しいブール変数が導入されました。この変数は、スタックトレースを生成すべきかどうかを制御します。

変更後のロジックは以下のようになります。

  1. 初期化: tracebackはデフォルトでtrueに設定されます。
  2. Windowsの特殊処理: Windows環境ではプロファイリングが専用のスレッドで行われるため、m(OSスレッド)やmcache(Mのキャッシュ)がnilの場合でもtracebacktrueのままです。それ以外のOSでは、mまたはmcachenilの場合はtracebackfalseに設定されます。これは、有効なMコンテキストがない場合にスタックトレースを試みても意味がないためです。
  3. 特殊なGoルーチンの除外: gp == m->g0(ランタイム内部処理用のGoルーチン)またはgp == m->gsignal(シグナルハンドラ用のGoルーチン)の場合、tracebackfalseに設定されます。これは、これらのGoルーチンはユーザーコードとは異なる特殊なスタック構造を持つため、通常のruntime·gentracebackでは正確なスタックトレースが得られない可能性があるためです。
  4. レース検出器の考慮: m != nil && m->racecallの場合、tracebackfalseに設定されます。これは、レース検出器が有効な場合にasmcgocallを介して行われる呼び出しは、スタックトレースが困難な特殊な状態になるためです。
  5. スタックトレースの試行: tracebacktrueの場合にのみ、runtime·gentracebackが呼び出され、スタックトレースが試みられます。
  6. フォールバックメカニズム: tracebackfalseの場合、またはruntime·gentracebackがスタックトレースを生成できなかった場合(n <= 0)、プロファイルデータはPC(プログラムカウンタ)とSystem関数のアドレス(+1)の2つのエントリで構成されます。
    • prof.pcbuf[0] = (uintptr)pc;: シグナルが発生した時点のプログラムカウンタ。
    • prof.pcbuf[1] = (uintptr)System + 1;: 新しく追加されたSystem関数のアドレス。このSystem関数は空の関数であり、そのアドレスをプロファイルに含めることで、スタックトレースが取得できないランタイム内部の処理(GC、スケジューラ、システムコールなど)を「System」というカテゴリで集計できるようにしています。+1は、Goのスタックトレースにおいて、関数の開始アドレスではなく、その関数内の任意の命令のアドレスを示す慣習的なオフセットです。これにより、System関数がプロファイル上で独立したエントリとして認識されやすくなります。
  7. プロファイルデータの記録: 最終的に、prof.fn(prof.pcbuf, n)が呼び出され、取得された(またはフォールバックされた)スタックトレースがプロファイラに記録されます。

runtime·sighandler関数の変更 (src/pkg/runtime/signal_*.c)

各アーキテクチャ固有のsignal_*.cファイルでは、SIGPROFシグナルを処理するruntime·sighandler関数から、以下の条件分岐が削除されました。

if(gp != m->g0 && gp != m->gsignal)
    runtime·sigprof(...)

この変更により、SIGPROFシグナルがm->g0m->gsignalで発生した場合でも、無条件にruntime·sigprof関数が呼び出されるようになりました。これにより、ランタイム内部の処理(GCなど)で発生したプロファイリング信号もruntime·sigprofに渡され、前述のフォールバックメカニズムによって「System」として集計される道が開かれました。

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

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1990,26 +1990,45 @@ static struct {
 	uintptr pcbuf[100];
 } prof;
 
+static void
+System(void)
+{
+}
+
 // Called if we receive a SIGPROF signal.
 void
 runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
 {
 	int32 n;
+	bool traceback;
 
-\t// Windows does profiling in a dedicated thread w/o m.\n-\tif(!Windows && (m == nil || m->mcache == nil))\n-\t\treturn;\n \tif(prof.fn == nil || prof.hz == 0)\n \t\treturn;\
+\ttraceback = true;
+\t// Windows does profiling in a dedicated thread w/o m.\n+\tif(!Windows && (m == nil || m->mcache == nil))\n+\t\ttraceback = false;
+\tif(gp == m->g0 || gp == m->gsignal)\
+\t\ttraceback = false;
+\t// Race detector calls asmcgocall w/o entersyscall/exitsyscall,\n+\t// we can not currently unwind through asmcgocall.\n+\tif(m != nil && m->racecall)\
+\t\ttraceback = false;
 
 \truntime·lock(&prof);\
 \tif(prof.fn == nil) {\
 \t\truntime·unlock(&prof);\
 \t\treturn;\
 \t}\
-\tn = runtime·gentraceback((uintptr)pc, (uintptr)sp, (uintptr)lr, gp, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);\
-\tif(n > 0)\
-\t\tprof.fn(prof.pcbuf, n);\
+\tn = 0;
+\tif(traceback)\
+\t\tn = runtime·gentraceback((uintptr)pc, (uintptr)sp, (uintptr)lr, gp, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);
+\tif(!traceback || n <= 0) {\
+\t\tn = 2;
+\t\tprof.pcbuf[0] = (uintptr)pc;
+\t\tprof.pcbuf[1] = (uintptr)System + 1;
+\t}
+\tprof.fn(prof.pcbuf, n);
 \truntime·unlock(&prof);\
 }

src/pkg/runtime/signal_386.c, src/pkg/runtime/signal_amd64.c, src/pkg/runtime/signal_arm.c

これらのファイルでは、runtime·sighandler関数内のSIGPROFシグナル処理部分から、gp != m->g0 && gp != m->gsignalという条件が削除されています。

例: src/pkg/runtime/signal_386.c

--- a/src/pkg/runtime/signal_386.c
+++ b/src/pkg/runtime/signal_386.c
@@ -39,8 +39,7 @@ runtime·sighandler(int32 sig, Siginfo *info, void *ctxt, G *gp)
 	bool crash;
 
 	if(sig == SIGPROF) {
-\t\tif(gp != m->g0 && gp != m->gsignal)\
-\t\t\truntime·sigprof((byte*)SIG_EIP(info, ctxt), (byte*)SIG_ESP(info, ctxt), nil, gp);\
+\t\truntime·sigprof((byte*)SIG_EIP(info, ctxt), (byte*)SIG_ESP(info, ctxt), nil, gp);\
 	\treturn;\
 	}

コアとなるコードの解説

このコミットの核心は、runtime·sigprof関数におけるプロファイリング信号の処理方法の変更と、runtime·sighandlerからの呼び出し条件の緩和です。

  1. System関数の導入: static void System(void) {}という空の関数が追加されました。この関数のアドレスは、スタックトレースが取得できない場合にプロファイルデータに含められます。これにより、GCやスケジューラなどのランタイム内部処理が「System」という共通のカテゴリで集計され、プロファイル上で可視化されるようになります。これは、これらの処理の正確なコールスタックを追跡することが困難であるため、次善の策として導入されたものです。

  2. traceback変数の導入と条件付きスタックトレース: runtime·sigprof関数内でbool traceback;が導入され、スタックトレースを試みるべきかどうかを動的に判断するようになりました。

    • gp == m->g0 || gp == m->gsignal: ランタイム内部のGoルーチンやシグナルハンドラ用のGoルーチンでは、通常のスタックトレースが困難なため、tracebackfalseになります。
    • m != nil && m->racecall: レース検出器が有効な場合の特定のasmcgocallのコンテキストでも、スタックトレースが困難なため、tracebackfalseになります。
    • これらの条件に合致する場合、runtime·gentracebackは呼び出されず、代わりにpcSystem関数のアドレスがプロファイルデータとして記録されます。これにより、これらの特殊なコンテキストでのCPU時間もプロファイルに反映されるようになります。
  3. runtime·sighandlerからの無条件呼び出し: signal_*.cファイルにおける変更は、SIGPROFシグナルがm->g0m->gsignalで発生した場合でも、runtime·sigprof関数が呼び出されるようにした点です。これにより、GCなどのランタイム内部処理中に発生したプロファイリング信号がruntime·sigprofに到達し、そこで新しいフォールバックメカニズムによって処理されるようになります。

これらの変更により、GoのCPUプロファイリングは、ユーザーコードだけでなく、ランタイム内部の重要な処理(GC、スタックスプリット、スケジューラ、システムコールなど)のCPU時間も正確に反映できるようになりました。これにより、プロファイルデータはより包括的で信頼性の高いものとなり、パフォーマンス分析の精度が向上します。

コミットメッセージに記載されているように、この変更によってsyscall.Syscallscanblockmarkonlysweepspanruntime.newstackといったエントリがプロファイルに現れるようになったのは、これらの処理が以前は適切にプロファイルされていなかったことを示しています。特にGC関連の関数(scanblock, markonly, sweepspan)がプロファイルに現れるようになったことは、GCのパフォーマンス特性を理解する上で非常に重要です。

関連リンク

参考にした情報源リンク

  • GoのCPUプロファイリングに関する公式ドキュメントやブログ記事
  • GoランタイムのM/P/Gモデルに関する解説
  • SIGPROFシグナルとUnix系システムにおけるプロファイリングの仕組み
  • Goのガベージコレクションの仕組み
  • Goのスタックスプリットに関する情報
  • Goのレース検出器に関する情報
  • Goのソースコード(特にsrc/runtimeディレクトリ)
  • Dmitriy Vyukov氏の他のGoランタイム関連コミットや議論