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

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

このコミットは、Goランタイムにおける以前の変更(コミットハッシュ 1e280889f997、CL 9776044)を元に戻すものです。元の変更はCPUの利用率の低さを改善することを目的としていましたが、ビルドボット上での失敗が確認されたため、その変更が取り消されました。

コミット

commit 7ebb187e8e5e588d8c594213ff5187917c4abb20
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Jun 27 21:03:35 2013 +0400

    undo CL 9776044 / 1e280889f997
    
    Failure on bot:
    http://build.golang.org/log/f4c648906e1289ec2237c1d0880fb1a8b1852a08
    
    ««« original CL description
    runtime: fix CPU underutilization
    runtime.newproc/ready are deliberately sloppy about waking new M's,
    they only ensure that there is at least 1 spinning M.
    Currently to compensate for that, schedule() checks if the current P
    has local work and there are no spinning M's, it wakes up another one.
    It does not work if goroutines do not call schedule.
    With this change a spinning M wakes up another M when it finds work to do.
    It's also not ideal, but it fixes the underutilization.
    A proper check would require to know the exact number of runnable G's,
    but it's too expensive to maintain.
    Fixes #5586.
    
    R=rsc
    TBR=rsc
    CC=gobot, golang-dev
    https://golang.org/cl/9776044
    »»»
    
    R=golang-dev
    CC=golang-dev
    https://golang.org/cl/10692043

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

https://github.com/golang/go/commit/7ebb187e8e5e588d8c594213ff5187917c4abb20

元コミット内容

元のコミット(CL 9776044 / 1e280889f997)は、GoランタイムにおけるCPUの利用率の低さを修正することを目的としていました。runtime.newprocready といった関数は、新しいM(OSスレッド)を起こす際に意図的に大雑把な挙動をしており、少なくとも1つのスピンしているMが存在することだけを保証していました。

この問題を補うため、schedule() 関数は、現在のP(プロセッサ)にローカルな作業があり、かつスピンしているMが存在しない場合に、別のMを起こすようにしていました。しかし、このアプローチは、ゴルーチンが schedule を呼び出さない場合には機能しないという問題がありました。

元のコミットでは、この問題を解決するために、作業を見つけたスピン中のMが別のMを起こすように変更されました。これは理想的な解決策ではないと認識されていましたが、CPUの利用率の低さを修正すると考えられていました。より適切なチェックには、実行可能なG(ゴルーチン)の正確な数を把握する必要がありましたが、これは維持するのにコストがかかりすぎると判断されていました。この変更は、Issue #5586 を修正することを意図していました。

変更の背景

このコミット(7ebb187e8e5e588d8c594213ff5187917c4abb20)は、上記の元のコミット(1e280889f997)を元に戻すものです。元に戻された理由は、Goのビルドボット上でこの変更が原因でテストが失敗したためです。具体的には、http://build.golang.org/log/f4c648906e1289ec2237c1d0880fb1a8b1852a08 のログで失敗が報告されています。

ビルドボットでの失敗は、元の変更が意図しない副作用や、特定の環境下での問題を引き起こしたことを示唆しています。Goのランタイムは非常に複雑であり、並行処理の挙動はデリケートであるため、一見すると正しいように見える最適化が、予期せぬデッドロックやパフォーマンスの低下、あるいはテストの失敗につながることがあります。この「undo」コミットは、安定性を優先し、問題のある変更を一時的に取り消すという、開発における一般的なプラクティスに従ったものです。

前提知識の解説

このコミットを理解するためには、Goランタイムのスケジューラに関する以下の基本的な概念を理解しておく必要があります。

  • G (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。
  • M (Machine/OS Thread): オペレーティングシステムのスレッドです。Goランタイムは、Gを実行するためにMを使用します。MはOSによってスケジュールされます。
  • P (Processor/Context): 論理プロセッサ、またはコンテキストです。MとGの間の仲介役として機能します。各Pは、実行可能なGのローカルキューを保持し、MがGを実行するためのコンテキストを提供します。GOMAXPROCS 環境変数によって、同時に実行できるPの数が決まります。
  • スケジューラ (Scheduler): GをMとPに割り当て、実行を管理するGoランタイムのコンポーネントです。Gがブロックされたり、実行を完了したりすると、スケジューラは別のGをPに割り当てて実行を継続します。
  • スピン (Spinning): Mが実行可能なGを探している状態を指します。Mは、Gが見つかるまでCPUを消費してループし続けることがあります。これは、新しいGがすぐに利用可能になる場合に、OSにスレッドをスリープさせてから再度起こすオーバーヘッドを避けるために行われます。しかし、スピンしすぎるとCPUを無駄に消費する可能性があります。
  • nmspinning: スピンしているMの数を追跡するカウンタです。Goランタイムは、このカウンタを使用して、システムに十分なスピン中のMがあるかどうかを判断し、必要に応じて新しいMを起こしたり、既存のMをスリープさせたりします。
  • wakep(): 新しいPを起こす(つまり、新しいMを起動してPにアタッチする)ための関数です。これは、利用可能なPが不足している場合や、既存のPがすべてブロックされている場合に、より多くのゴルーチンを並行して実行できるようにするために行われます。
  • schedule(): Goランタイムのスケジューラの中核となる関数で、現在のゴルーチンの実行を中断し、次に実行すべきゴルーチンを選択します。
  • findrunnable(): 実行可能なゴルーチンを見つけるための関数です。ローカルPのキュー、グローバルキュー、ネットワークポーラーなどからゴルーチンを探します。

技術的詳細

元のコミット(1e280889f997)は、src/pkg/runtime/proc.c 内の findrunnable 関数と schedule 関数の挙動を変更することで、CPUの利用率の低さを改善しようとしました。

具体的には、元の変更では以下の点が導入されました。

  1. findrunnable1 の導入: 既存の findrunnable 関数を findrunnable1 にリネームし、findrunnable という新しいラッパー関数を導入しました。この新しい findrunnable は、findrunnable1 が作業を見つけてブロックから戻った後に、現在のMがスピン状態から抜け出す処理 (m->spinning = false; runtime·xadd(&runtime·sched.nmspinning, -1);) と、必要に応じて別のPを起こす (wakep()) ロジックを含んでいました。

    • 元の問題意識: runtime.newproc/ready が新しいMを起こすのが大雑把であるため、schedule() が現在のPにローカルな作業があり、スピンしているMがいない場合にのみ wakep() を呼ぶという既存の補償メカニズムでは不十分でした。特に、ゴルーチンが schedule を頻繁に呼び出さない場合に問題が生じました。
    • 元の変更の意図: 作業を見つけたスピン中のMが自ら別のMを起こすことで、より積極的に並行性を高め、CPUの利用率を改善しようとしました。
  2. schedule 内の wakep() 呼び出しの変更: schedule 関数内で、findrunnable() が呼び出された後に、Mのスピン状態を解除する処理と、wakep() を呼び出す条件が変更されました。

    • 元の変更では、schedule 内の wakep() の条件が m->p->runqhead != m->p->runqtail (現在のPのローカルキューに作業がある) かつ runtime·atomicload(&runtime·sched.nmspinning) == 0 (スピンしているMがいない) かつ runtime·atomicload(&runtime·sched.npidle) > 0 (アイドル状態のPがある) となっていました。これは、元の findrunnable が呼び出された後に、現在のMがローカルキューに作業を見つけた場合に、他のMを起こすというロジックを反映していました。

この「undo」コミットは、これらの変更を完全に元に戻します。つまり、findrunnable1 は削除され、findrunnable は元のシンプルな形に戻ります。また、schedule 関数内のMのスピン状態解除と wakep() 呼び出しのロジックも、元のコミットが適用される前の状態に戻されます。

src/pkg/runtime/proc_test.go から TestGoroutineParallelism テストが削除されていることも注目に値します。このテストは、元のコミットが導入した並行性の改善を検証するためのものであった可能性が高く、変更が元に戻されたため、このテストも不要になったか、あるいはテスト自体が問題の原因の一部であった可能性も考えられます。

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

src/pkg/runtime/proc.c

  • findrunnable1 関数が削除され、findrunnable 関数が元のシンプルな実装に戻されました。
    • 元の findrunnable 関数(現在の findrunnable1 に相当)は、実行可能なGが見つかるまでブロックするロジックのみを含んでいました。
    • 元のコミットで追加された、Mのスピン状態解除と wakep() を呼び出すロジックが削除されました。
  • schedule 関数内の findrunnable() 呼び出し後のMのスピン状態解除と wakep() 呼び出しのロジックが削除され、元の状態に戻されました。

src/pkg/runtime/proc_test.go

  • TestGoroutineParallelism 関数が完全に削除されました。

コアとなるコードの解説

src/pkg/runtime/proc.c の変更

元のコミットでは、findrunnable 関数が以下のように変更されていました(簡略化)。

// 元のコミットで追加された findrunnable
static G*
findrunnable(void)
{
    G *gp;
    int32 nmspinning;

    gp = findrunnable1();  // blocks until work is available
    if(m->spinning) {
        m->spinning = false;
        nmspinning = runtime·xadd(&runtime·sched.nmspinning, -1);
        if(nmspinning < 0)
            runtime·throw("findrunnable: negative nmspinning");
    } else
        nmspinning = runtime·atomicload(&runtime·sched.nmspinning);

    // M wakeup policy is deliberately somewhat conservative (see nmspinning handling),
    // so see if we need to wakeup another P here.
    if (nmspinning == 0 && runtime·atomicload(&runtime·sched.npidle) > 0)
        wakep();

    return gp;
}

この「undo」コミットでは、上記の findrunnable 関数が削除され、findrunnable1findrunnable にリネームされ、元のシンプルな実装に戻されました。

また、schedule 関数内の変更も元に戻されました。元のコミットでは、schedule 関数は以下のようなロジックを含んでいました(簡略化)。

// 元のコミットでの schedule 関数の一部
    if(gp == nil)
        gp = findrunnable();  // blocks until work is available

    if(m->spinning) {
        m->spinning = false;
        runtime·xadd(&runtime·sched.nmspinning, -1);
    }

    // M wakeup policy is deliberately somewhat conservative (see nmspinning handling),
    // so see if we need to wakeup another M here.
    if (m->p->runqhead != m->p->runqtail &&
        runtime·atomicload(&runtime·sched.nmspinning) == 0 &&
        runtime·atomicload(&runtime·sched.npidle) > 0)
        wakep();

この「undo」コミットでは、schedule 関数内の上記の if(m->spinning) ブロックとそれに続く wakep() の呼び出しが削除され、元の状態に戻されました。これにより、Mのスピン状態の管理と wakep() の呼び出しは、元のコミットが導入する前のロジックに戻ります。

src/pkg/runtime/proc_test.go の変更

TestGoroutineParallelism テストは、複数のP(プロセッサ)が同時にゴルーチンをスケジュールし、並行して実行できることを検証するためのものでした。このテストは、atomic.LoadUint32atomic.StoreUint32 を使用して、複数のゴルーチンが共有変数 x を特定の順序で更新できることを確認していました。

このテストが削除されたのは、元のコミットが元に戻されたため、そのコミットが意図した並行性の挙動がもはや期待されないか、あるいはこのテスト自体が元のコミットの失敗の原因を特定する上で役立たなかったためと考えられます。テストが失敗していた可能性もあります。

関連リンク

参考にした情報源リンク

  • Goのスケジューラに関する公式ドキュメントやブログ記事 (Goのバージョンによって詳細が異なる場合がありますが、M, P, Gの基本的な概念は共通です)
  • Goのソースコード (src/pkg/runtime/proc.c および関連ファイル)
  • GoのIssueトラッカー (特に #5586)
  • Goのコードレビューシステム (Gerrit) のCLページ
  • Goのビルドボットのログ