[インデックス 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