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

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

このコミットは、GoランタイムにおけるARMアーキテクチャ上での整数除算のプロファイリングとトレースバックに関する以前の変更(CL 19810043 / コミットハッシュ 352f3b7c9664)を元に戻すものです。元の変更がmisc/cgo/testでランダムなテスト失敗を引き起こしたため、その問題を解決するために以前の状態に戻されました。

コミット

commit b88148b9a04e22cc338834ca405fc1333a1bd5d7
Author: Russ Cox <rsc@golang.org>
Date:   Thu Oct 31 17:18:57 2013 +0000

    undo CL 19810043 / 352f3b7c9664
    
    The CL causes misc/cgo/test to fail randomly.
    I suspect that the problem is the use of a division instruction
    in usleep, which can be called while trying to acquire an m
    and therefore cannot store the denominator in m.
    The solution to that would be to rewrite the code to use a
    magic multiply instead of a divide, but now we're getting
    pretty far off the original code.
    
    Go back to the original in preparation for a different,
    less efficient but simpler fix.
    
    ««« original CL description
    cmd/5l, runtime: make ARM integer division profiler-friendly
    
    The implementation of division constructed non-standard
    stack frames that could not be handled by the traceback
    routines.
    
    CL 13239052 left the frames non-standard but fixed them
    for the specific case of a divide-by-zero panic.
    A profiling signal can arrive at any time, so that fix
    is not sufficient.
    
    Change the division to store the extra argument in the M struct
    instead of in a new stack slot. That keeps the frames bog standard
    at all times.
    
    Also fix a related bug in the traceback code: when starting
    a traceback, the LR register should be ignored if the current
    function has already allocated its stack frame and saved the
    original LR on the stack. The stack copy should be used, as the
    LR register may have been modified.
    
    Combined, these make the torture test from issue 6681 pass.
    
    Fixes #6681.
    
    R=golang-dev, r, josharian
    CC=golang-dev
    https://golang.org/cl/19810043
    »»»
    
    TBR=r
    CC=golang-dev
    https://golang.org/cl/20350043

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

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

元コミット内容

このコミットが元に戻したCL 19810043(コミットハッシュ 352f3b7c9664)は、以下の2つの主要な目的を持っていました。

  1. ARM整数除算のプロファイラフレンドリー化: ARMアーキテクチャにおける整数除算の実装が、標準的ではないスタックフレームを構築していました。これにより、Goのプロファイリングツール(pprof)がスタックトレースを正確に取得できず、プロファイリングの精度が低下する問題がありました。元のCLは、除算に必要な追加引数を新しいスタック領域ではなく、M(Machine/Thread)構造体に保存するように変更することで、スタックフレームを常に標準的な状態に保ち、プロファイリングを容易にしようとしました。
  2. トレースバックコードのバグ修正: トレースバックを開始する際に、現在の関数が既にスタックフレームを割り当て、元のLR(Link Register)をスタックに保存している場合、LRレジスタ自体は変更されている可能性があるため無視されるべきでした。元のCLは、この状況でスタックに保存されたLRを使用するようにトレースバックコードを修正しました。

これらの変更は、Issue 6681で報告された「torture test」をパスさせることを目的としていました。

変更の背景

このコミットは、GoランタイムにおけるARMアーキテクチャでの整数除算の取り扱いに関する複雑な問題と、プロファイリングおよびトレースバックの正確性に関する課題を浮き彫りにしています。

元の変更(CL 19810043)が導入された背景: Goのプロファイリングツールpprofは、プログラムの実行中に定期的にサンプリングを行い、その時点のスタックトレースを記録することで、CPU時間の消費状況を分析します。しかし、ARMアーキテクチャにおける整数除算の実装が、標準的なスタックフレームの規約に従わない場合がありました。これにより、pprofが正確なスタックトレースを収集できず、除算処理がプロファイルに適切に反映されないという問題が発生していました。また、トレースバック(パニック時などに呼び出し履歴を表示する機能)においても、LRレジスタの不適切な使用がバグを引き起こしていました。CL 19810043は、これらの問題を解決し、プロファイリングとトレースバックの信頼性を向上させることを目指しました。特に、除算の引数をM構造体に保存することで、スタックフレームのレイアウトを標準化し、プロファイラが容易にスタックを辿れるようにする意図がありました。

今回のコミット(CL 20350043)で元の変更が元に戻された背景: CL 19810043が導入された後、misc/cgo/testというCgo関連のテストがランダムに失敗するという新たな問題が発生しました。コミットメッセージによると、この問題はusleep関数内で除算命令が使用されており、かつその除算がm(GoランタイムのスケジューラにおけるOSスレッドを表す構造体)を取得しようとしている最中に呼び出される可能性があることに起因すると推測されています。

m構造体は、Goランタイムの重要なコンテキスト情報(現在のゴルーチン、スタック情報など)を保持しています。元のCL 19810043では、除算の引数をm構造体のdivmodフィールドに保存するように変更されていました。しかし、usleepのような低レベルの関数がmを取得しようとしている最中に除算が行われ、その際にm構造体へのアクセスが競合したり、不完全な状態のmに書き込もうとしたりすることで、ランダムなテスト失敗が発生したと考えられます。

コミットの作者は、この問題を解決するために除算を「マジック乗算」(除算を乗算とビットシフトで近似する最適化手法)に書き換えることも検討しましたが、それは元のコードから大きく逸脱することになると判断しました。そのため、よりシンプルで安全な解決策を模索する準備として、一旦元の変更を元に戻すことが決定されました。これは、複雑な最適化や低レベルのランタイム変更が、予期せぬ副作用や競合状態を引き起こす可能性があることを示しています。

前提知識の解説

Goランタイムの基本概念

  • Goroutine (G): Goにおける軽量な実行単位。OSスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。
  • Machine (M): OSスレッドを表すGoランタイムの構造体。Goのスケジューラは、GをM上で実行します。MはOSスレッドと1対1で対応し、CPU時間、レジスタ、スタックなどのOSレベルのリソースを管理します。
  • Processor (P): MとGを仲介する論理プロセッサ。Goスケジューラは、PにGを割り当て、PがMにGを実行させます。Pの数は通常、CPUのコア数に設定されます。
  • スケジューラ: GをMに割り当て、実行を管理するGoランタイムの中核部分。Gの生成、停止、再開、Mの管理などを行います。
  • m構造体: src/pkg/runtime/runtime.hで定義されるM構造体は、特定のOSスレッドに関連する状態を保持します。これには、現在のゴルーチン(g0)、スタック情報、そして以前のCLで追加されたdivmodのようなアーキテクチャ固有のデータが含まれることがあります。

ARMアーキテクチャの基本

  • レジスタ: ARMプロセッサは、汎用レジスタ(R0-R12)、スタックポインタ(SP, R13)、リンクレジスタ(LR, R14)、プログラムカウンタ(PC, R15)などを持っています。
    • SP (Stack Pointer): 現在のスタックの最上位を指すレジスタ。
    • LR (Link Register): 関数呼び出し時に、呼び出し元に戻るアドレス(リターンアドレス)を保存するレジスタ。関数から戻る際には、このLRの値がPCにロードされます。
  • スタックフレーム: 関数が呼び出されるたびに、スタック上にその関数専用の領域(スタックフレーム)が確保されます。ここには、ローカル変数、引数、そして呼び出し元に戻るためのリターンアドレス(LRの値)などが保存されます。
  • 整数除算: ARMプロセッサには、ハードウェアによる整数除算命令を持つものと、ソフトウェアエミュレーション(除算ルーチン)で実現するものがあります。ソフトウェアエミュレーションの場合、除算は複数の命令や関数呼び出しによって実現されるため、そのスタックフレームの構造が複雑になることがあります。

Goプロファイリング (pprof)

  • サンプリングプロファイラ: Goのpprofは、サンプリングベースのCPUプロファイラです。これは、一定の間隔(例: 100Hz、つまり1秒間に100回)で実行中のプログラムを一時停止し、その時点のスタックトレースを記録します。
  • スタックトレース: プロファイリング時に記録されるスタックトレースは、現在実行中の関数から、その関数を呼び出した関数、さらにその呼び出し元へと遡る関数呼び出しの連鎖です。これにより、どの関数がCPU時間を多く消費しているかを特定できます。
  • プロファイラフレンドリー: プロファイラが正確な情報を収集できるようなコードの特性を指します。特に、スタックフレームの構造が標準的で予測可能であることは、プロファイラがスタックトレースを正しく辿る上で非常に重要です。非標準的なスタックフレームは、プロファイラがスタックを正しくアンワインド(巻き戻し)できず、プロファイルデータが不正確になる原因となります。

トレースバック

  • トレースバック(スタックトレース): プログラムがパニックを起こしたり、エラーが発生したりした際に、その時点までの関数呼び出しの履歴を表示する機能です。デバッグにおいて非常に重要な情報となります。
  • LRレジスタとトレースバック: ARMアーキテクチャでは、LRレジスタがリターンアドレスを保持するため、トレースバックの際にスタックを辿る上で重要な役割を果たします。しかし、関数内でLRレジスタが一時的に別の目的で使用されたり、スタックに保存されたLRが最新でなかったりする場合、トレースバックが誤った情報を表示する可能性があります。

Cgo

  • Cgo: GoプログラムからC言語のコードを呼び出すためのGoの機能です。Cgoを使用すると、GoとCの関数間でデータをやり取りしたり、Cライブラリの機能を利用したりできます。misc/cgo/testは、Cgoの機能が正しく動作するかを検証するためのテストスイートです。Cgoの呼び出しは、GoランタイムとCランタイムの間でコンテキストスイッチやスタックの切り替えを伴うため、低レベルのランタイム変更が影響を与えやすい領域です。

技術的詳細

このコミットは、GoランタイムにおけるARMアーキテクチャでの整数除算の実装と、それに伴うプロファイリングおよびトレースバックの問題に対する以前の試みを元に戻すものです。

元に戻されたCL 19810043の技術的詳細:

CL 19810043は、ARMの整数除算が非標準的なスタックフレームを構築するという問題に対処しようとしました。通常、関数呼び出しでは、引数やローカル変数、リターンアドレスなどがスタックフレームに格納されます。しかし、ARMの除算ルーチンは、プロファイラが正しくスタックを辿れないような特殊なフレームを生成していました。

この問題を解決するため、CL 19810043は以下の変更を導入しました。

  1. M構造体への除算引数の保存: 除算に必要な追加の引数(特に除数)を、新しいスタック領域に保存する代わりに、M構造体(src/pkg/runtime/runtime.huint32 divmod;として追加された)に保存するように変更しました。これにより、除算ルーチンが標準的なスタックフレームを維持できるようになり、プロファイラがスタックトレースを正確に取得できることを期待しました。
  2. トレースバックロジックの修正: src/pkg/runtime/traceback_arm.cにおいて、トレースバック時にLRレジスタの扱いを修正しました。具体的には、関数が既にスタックフレームを割り当て、元のLRをスタックに保存している場合、LRレジスタ自体が変更されている可能性があるため、スタックに保存されたLRを使用するように変更しました。これは、Issue 6681で報告された特定の「torture test」をパスさせるためのものでした。
  3. アセンブリコードの変更: src/pkg/runtime/vlop_arm.s内の除算ルーチン(_divu, _modu, _div, _mod)が、引数をスタックではなくm_divmod(m)M構造体のdivmodフィールド)から取得するように変更されました。また、udiv_by_0(ゼロ除算パニック)のハンドリングも、トレースバックが正しく機能するようにスタックをアンワインドするロジックが追加されました。
  4. リンカの変更: src/cmd/5l/noop.cでは、ARMの除算命令(DIV, MOD)が擬似命令として扱われ、リンカによって実際の除算ルーチンへの呼び出しに展開されます。CL 19810043では、この展開ロジックが変更され、除算の引数をM構造体に渡すためのコードが生成されるようになりました。

今回のコミット(b88148b9a04e22cc338834ca405fc1333a1bd5d7)による元戻し:

CL 19810043の導入後、misc/cgo/testがランダムに失敗するという問題が発生しました。コミットメッセージによると、この問題はusleep関数内で除算命令が使用されており、その除算がm(OSスレッドを表すGoランタイムの構造体)を取得しようとしている最中に呼び出される可能性があることに起因すると推測されています。

Goランタイムでは、ゴルーチンがOSスレッド(M)上で実行されます。usleepのようなシステムコールに近い関数が、mのコンテキストに依存する除算を行うと、mが完全に初期化されていない、あるいは別のゴルーチンによって変更されている途中の状態である場合に、m->divmodへのアクセスが問題を引き起こす可能性があります。これは、低レベルのランタイム操作における繊細な競合状態を示唆しています。

この問題を解決するために、作者はCL 19810043の変更を完全に元に戻すことを選択しました。これは、一時的な後退ではありますが、より安定した基盤を確保し、将来的に異なる、より堅牢な解決策を導入するための準備と位置づけられています。具体的には、M構造体からdivmodフィールドを削除し、除算ルーチンが再びスタックベースの引数渡しに戻り、トレースバックロジックも元の状態に戻されました。

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

このコミットは、以前の変更を元に戻すため、主に以下のファイルで削除や元のコードへの復元が行われています。

  1. src/cmd/5l/noop.c:

    • リンカがARMの除算命令を処理する際のコード生成ロジックが元に戻されました。
    • Prog *p, *q, *q1, *q2, orig; の行から orig 変数の宣言が削除され、q1 = p; が追加されました。これは、以前の変更で導入されたorig構造体(元の命令のコピーを保持していた)の使用を廃止し、q1というポインタで元の命令を参照するように戻したことを意味します。
    • MOV a,4(M)MOV a,4(SP) のような命令の生成において、Mレジスタ(REGM)ではなくSPレジスタ(REGSP)を使用するように戻され、オフセットも4(SP)に戻されました。これは、除算の引数をM構造体ではなくスタックに保存するように戻したことを示します。
    • ADD $8,SPSUB $8,SP の命令が追加され、スタックポインタの調整が明示的に行われるようになりました。これは、除算ルーチンがスタックフレームを自身で調整するように戻ったことを意味します。
  2. src/pkg/runtime/pprof/pprof_test.go:

    • TestMathBigDivideというテスト関数が完全に削除されました。このテストは、Issue 6681に関連して、ARM上での除算操作のプロファイリングが適切に行われることを検証するために追加されたものでした。元の変更が元に戻されたため、このテストも不要となりました。
    • math/bigパッケージのインポートも削除されました。
  3. src/pkg/runtime/runtime.h:

    • struct M定義から uint32 divmod; // div/mod denominator on arm の行が削除されました。これは、ARMの整数除算の除数をM構造体に保存するという以前の変更が完全に撤回されたことを意味します。
  4. src/pkg/runtime/traceback_arm.c:

    • runtime·gentraceback関数内のトレースバックロジックが元に戻されました。具体的には、if((n == 0 && frame.fp > frame.sp) || frame.lr == 0) の条件が if(frame.lr == 0) に戻されました。これは、トレースバック時にLRレジスタの扱いに関する以前の修正が撤回されたことを示します。
  5. src/pkg/runtime/vlop_arm.s:

    • ARMアセンブリで記述された整数除算ルーチン(_divu, _modu, _div, _mod)の変更が元に戻されました。
    • これらのルーチンが、除数(denominator)をm_divmod(m)M構造体のdivmodフィールド)から取得するのではなく、0(FP)(フレームポインタからのオフセット、つまりスタック)から取得するように戻されました。
    • udiv_by_0(ゼロ除算パニック)のハンドリングも、以前の複雑なスタックアンワインドロジックが削除され、よりシンプルなB runtime·panicdivide(SB)runtime·panicdivide関数への直接ジャンプ)に戻されました。これは、ゼロ除算時のトレースバックの複雑な修正が撤回されたことを意味します。
    • TEXTディレクティブのスタックフレームサイズ指定が、$-4-0から$-4$16-0から$16に戻されました。これは、スタックフレームのレイアウトが以前の標準的な形式に戻ったことを示唆します。

コアとなるコードの解説

このコミットのコアとなる変更は、GoランタイムがARMアーキテクチャ上で整数除算をどのように処理し、その際にスタックフレームとプロファイリングにどのような影響を与えるかという点に集約されます。

src/pkg/runtime/runtime.h の変更: M構造体からdivmodフィールドが削除されたことは、最も直接的な変更です。以前のCLでは、ARMの整数除算の除数をこのフィールドに一時的に保存することで、除算ルーチンがスタックに余分な領域を確保する必要がなくなり、スタックフレームを「標準的」に保つことを目指していました。しかし、このアプローチがusleepのような低レベルの関数で問題を引き起こしたため、このフィールドは撤回されました。これにより、除算ルーチンは再びスタックを使用して引数を渡すことになります。

src/pkg/runtime/vlop_arm.s の変更: ARMアセンブリで記述された除算ルーチン(_divu, _modu, _div, _mod)の変更は、このコミットの核心部分です。

  • 引数渡しの変更: 以前はm_divmod(m)から除数を取得していましたが、このコミットでは0(FP)(フレームポインタからのオフセット)から取得するように戻されました。これは、除数が再びスタックを通じて渡されるようになったことを意味します。スタックを使用することで、M構造体への競合アクセスや、mが不完全な状態であることによる問題が回避されます。
  • ゼロ除算ハンドリングの簡素化: udiv_by_0ラベルのコードが大幅に簡素化されました。以前のCLでは、ゼロ除算パニック時にトレースバックが正しく機能するように、スタックをアンワインドし、LRレジスタを調整する複雑なロジックが追加されていました。しかし、このコミットでは、単にruntime·panicdivide関数に直接ジャンプするように戻されました。これは、以前の複雑な修正が不安定性の原因となっていた可能性があり、よりシンプルで既知の動作に戻すことを優先したためと考えられます。

src/cmd/5l/noop.c の変更: リンカのコード生成ロジックの変更は、アセンブリコードの変更と密接に関連しています。リンカは、Goのソースコード中の除算操作を、ARMアーキテクチャ上で実行可能なアセンブリ命令シーケンスに変換します。以前のCLでは、このシーケンスがM構造体を通じて引数を渡すように調整されていましたが、このコミットでは、スタックを通じて引数を渡し、スタックポインタを適切に調整する(ADD $8,SP, SUB $8,SP)ように戻されました。これにより、リンカが生成するコードが、vlop_arm.sの変更と整合性が取れるようになります。

src/pkg/runtime/traceback_arm.c の変更: トレースバックロジックの変更は、LRレジスタの扱いに関する以前の修正を元に戻すものです。以前の修正は、特定の状況下でLRレジスタが誤ったリターンアドレスを保持している可能性に対処しようとしましたが、このコミットではその修正が撤回されました。これは、その修正自体が新たな問題を引き起こしたか、あるいはより根本的な解決策が必要であると判断されたためと考えられます。

src/pkg/runtime/pprof/pprof_test.go の変更: TestMathBigDivideテストの削除は、以前の変更が元に戻された結果です。このテストは、ARM上での除算のプロファイリングが正しく機能することを確認するために存在していましたが、元の変更が撤回されたため、このテストも不要となりました。これは、プロファイリングの「フレンドリーさ」を向上させるという元の目標が、一旦後回しにされたことを示唆しています。

全体として、このコミットは、Goランタイムの低レベルな部分、特に特定のアーキテクチャ(ARM)におけるシステムコールやスケジューリングと密接に関連する操作(整数除算)の変更が、いかに予期せぬ複雑な副作用を引き起こすかを示しています。安定性と信頼性を優先し、一旦元の状態に戻すことで、より慎重なアプローチで問題解決に取り組むための基盤を再構築したと言えます。

関連リンク

参考にした情報源リンク