[インデックス 13714] ファイルの概要
このコミットは、Go言語のcgo
テストスイート内のmisc/cgo/test/issue1560.go
ファイルに対する変更です。主な目的は、ARMアーキテクチャ上でのsleep
関数の精度を向上させることです。具体的には、バックグラウンドでCPUを消費するゴルーチンを導入することで、OSの電源管理機能がCPU周波数を引き上げるように仕向け、その結果としてsleep
の精度が改善されるというメカニズムを利用しています。
コミット
commit d073677569085642935d5d3b6acfe6df1054ae21
Author: Dave Cheney <dave@cheney.net>
Date: Fri Aug 31 20:17:59 2012 +1000
cgo/misc/test: burn CPU to improve sleep accuracy
Fixes #4008.
Run a background goroutine that wastes CPU to trick the
power management into raising the CPU frequency which,
by side effect, makes sleep more accurate on arm.
=== RUN TestParallelSleep
--- PASS: TestParallelSleep (1.30 seconds)
_cgo_gotypes.go:772: sleep(1) slept for 1.000458s
R=minux.ma, r
CC=golang-dev
https://golang.org/cl/6498060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d073677569085642935d5d3b6acfe6df1054ae21
元コミット内容
このコミットは、Go言語のcgo
テストの一部であるissue1560.go
ファイルに対して行われました。元のコードでは、ARMアーキテクチャの場合にsleep
の時間を2秒に増やすという回避策が講じられていました。これは、ARM環境でのsleep
の精度が低いことに対する暫定的な対応でした。
変更の背景
この変更は、Go Issue #4008「TestParallelSleep
fails on ARM due to inaccurate sleep」を修正するために行われました。ARMプロセッサを搭載したシステムでは、省電力機能が積極的に働き、CPUがアイドル状態になると周波数を下げることが一般的です。これにより、time.Sleep
のような時間ベースの関数が期待通りの精度で動作しないという問題が発生していました。特にテスト環境では、正確な時間計測が求められるため、この問題はテストの信頼性を損なう要因となっていました。
開発者は、この問題を解決するために、CPUを意図的に消費させることで、OSの電源管理がCPU周波数を高めるように誘導するというアプローチを採用しました。これにより、sleep
の精度が向上し、テストが安定してパスするようになります。
前提知識の解説
1. ARMアーキテクチャと電源管理
ARMプロセッサは、スマートフォンや組み込みシステムなど、電力効率が重視されるデバイスで広く使用されています。これらのシステムでは、バッテリー寿命を延ばすために、CPUの周波数や電圧を動的に調整する「電源管理」機能が搭載されています。CPUがアイドル状態になると、周波数を下げて消費電力を抑えます(CPUクロックゲーティング、ダイナミックボルテージ&フリークエンシースケーリング (DVFS) など)。
2. time.Sleep
の精度とCPU周波数
time.Sleep
関数は、指定された期間だけゴルーチンを一時停止させます。この関数の精度は、システムタイマーの分解能と、CPUの動作周波数に依存します。CPU周波数が低い場合、タイマー割り込みの頻度が減少し、結果としてsleep
の精度が低下する可能性があります。特に、短いsleep
時間の場合、この影響は顕著になります。
3. Go言語のruntime.GOMAXPROCS
runtime.GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を設定する関数です。デフォルトでは、利用可能なCPUコア数に設定されます。この値は、Goプログラムの並行処理の挙動に影響を与えます。GOMAXPROCS
を2に設定することで、少なくとも2つのOSスレッドが利用可能になり、CPUを消費するゴルーチンとテストを実行するゴルーチンが並行して動作する可能性が高まります。
4. cgo
cgo
は、GoプログラムからC言語のコードを呼び出すためのGoの機能です。このコミットがcgo/misc/test
ディレクトリ内のファイルに関連していることから、cgo
を使ったテスト環境でsleep
の精度が問題になっていたことが伺えます。
技術的詳細
このコミットの核心は、wasteCPU
という新しい関数と、それをTestParallelSleep
テスト内で利用する方法にあります。
wasteCPU
関数の実装
// wasteCPU starts a background goroutine to waste CPU
// to cause the power management to raise the CPU frequency.
// On ARM this has the side effect of making sleep more accurate.
func wasteCPU() chan struct{} {
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
default:
}
}
}()
// pause for a short amount of time to allow the
// power management to recognise load has risen.
<-time.After(300 * time.Millisecond)
return done
}
done := make(chan struct{})
:done
チャネルは、wasteCPU
ゴルーチンを停止させるためのシグナルとして使用されます。struct{}
はメモリを消費しない空の構造体で、シグナル用途によく使われます。go func() { ... }()
: 新しいゴルーチンを起動します。このゴルーチンがCPUを消費する役割を担います。for { select { case <-done: return default: } }
: この無限ループがCPUを「燃焼」させる部分です。select
ステートメントは、複数のチャネル操作を待機するために使用されます。case <-done:
:done
チャネルから値が送信された場合(つまり、close(done)
が呼び出された場合)、ゴルーチンはreturn
して終了します。default:
:done
チャネルからの値がない場合、default
ケースが即座に実行されます。default
ケースには何も処理が書かれていないため、このゴルーチンはCPUを解放することなく、ひたすらループを回し続けます。これにより、CPUは常に何らかの処理を行っている状態になり、アイドル状態に陥るのを防ぎます。
<-time.After(300 * time.Millisecond)
:wasteCPU
ゴルーチンを起動した後、メインゴルーチンは300ミリ秒間一時停止します。この短い遅延は、OSの電源管理機能がCPU負荷の上昇を認識し、CPU周波数を引き上げるための時間を与えることを目的としています。
testParallelSleep
の変更点
func testParallelSleep(t *testing.T) {
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(2))
defer close(wasteCPU())
sleepSec := 1
// ... (ARM固有のsleepSec調整ロジックが削除された)
start := time.Now()
parallelSleep(sleepSec)
dt := time.Since(start) // time.Now().Sub(start) から変更
t.Logf("sleep(%d) slept for %v", sleepSec, dt)
// ...
}
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(2))
:runtime.GOMAXPROCS(2)
は、Goランタイムが同時に実行できるOSスレッドの最大数を2に設定します。これにより、CPUを消費するゴルーチンとテストのロジックが並行して実行されることが保証されやすくなります。defer
キーワードにより、この設定はtestParallelSleep
関数が終了する際に元の値に戻されます。これはテストの副作用を避けるための良いプラクティスです。
defer close(wasteCPU())
:wasteCPU()
を呼び出してCPUを消費するゴルーチンを起動し、そのdone
チャネルを受け取ります。defer close(...)
により、testParallelSleep
関数が終了する際に、wasteCPU
ゴルーチンに停止シグナルが送られ、リソースが適切に解放されます。
- ARM固有の
sleepSec
調整の削除: 以前のコードにあったARMアーキテクチャの場合にsleepSec
を2秒に増やすという回避策が削除されました。これは、wasteCPU
の導入によりsleep
の精度が向上し、もはやこの回避策が不要になったことを示しています。 dt := time.Since(start)
: 時間計測の方法がtime.Now().Sub(start)
からtime.Since(start)
に変更されました。機能的には同じですが、time.Since
の方がより簡潔な記述です。t.Logf("sleep(%d) slept for %v", sleepSec, dt)
: 実際のsleep
時間をテストログに出力するようになりました。これにより、テストのデバッグや挙動の確認が容易になります。
コアとなるコードの変更箇所
変更はmisc/cgo/test/issue1560.go
ファイルに集中しています。
BackgroundSleep
関数の定義の下に、新しくwasteCPU
関数が追加されました。testParallelSleep
関数内で、runtime.GOMAXPROCS
の設定とwasteCPU
ゴルーチンの起動・停止ロジックが追加されました。testParallelSleep
関数内のARM固有のsleepSec
調整ロジックが削除されました。- 時間計測とログ出力の行が修正されました。
コアとなるコードの解説
このコミットのコアは、wasteCPU
関数がバックグラウンドでCPUを積極的に使用し続けることで、OSの電源管理がCPU周波数を高周波モードに維持するように「騙す」点にあります。これにより、time.Sleep
が依存するシステムタイマーの精度が向上し、結果としてsleep
の実行時間がより正確になります。
defer close(wasteCPU())
という記述は、Goのdefer
とチャネルの組み合わせの典型的な使用例です。wasteCPU()
がゴルーチンを起動し、そのゴルーチンを停止させるためのチャネルを返します。defer close(...)
とすることで、testParallelSleep
関数が正常終了するか、パニックで終了するかにかかわらず、CPUを消費するゴルーチンが確実に停止されることが保証されます。これはリソース管理において非常に重要です。
runtime.GOMAXPROCS(2)
の設定は、CPUを消費するゴルーチンが、テストの他の部分と並行して実行されることを保証するために重要です。もしGOMAXPROCS
が1に設定されていた場合、CPUを消費するゴルーチンが実行されている間、テストの他の部分がブロックされ、意図した効果が得られない可能性があります。
関連リンク
- Go Issue #4008: https://github.com/golang/go/issues/4008
- Go CL 6498060: https://golang.org/cl/6498060
参考にした情報源リンク
- Go言語の
time.Sleep
に関するドキュメント: https://pkg.go.dev/time#Sleep - Go言語の
runtime.GOMAXPROCS
に関するドキュメント: https://pkg.go.dev/runtime#GOMAXPROCS - ARMプロセッサの電源管理(DVFSなど)に関する一般的な情報源 (例: Wikipedia, ARM公式ドキュメントなど)
- Go言語における
defer
ステートメントの利用: https://go.dev/blog/defer-panic-recover - Go言語におけるチャネルの利用: https://go.dev/tour/concurrency/2
- Go言語のテストに関するドキュメント: https://pkg.go.dev/testing