[インデックス 13618] ファイルの概要
このコミットは、GoランタイムがLinuxシステム上で利用可能なCPUコア数を決定する方法を改善するものです。具体的には、runtime.NumCPU()
関数が、従来の/proc/stat
ファイルのパースに依存する方法から、より正確で堅牢なsched_getaffinity
システムコールを使用する方法へと変更されました。これにより、Goプログラムがシステムのリソースをより適切に認識し、利用できるようになります。
コミット
commit 4f308edc864a87ff4f6f9d6c531733cb1bf100f1
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Fri Aug 10 10:05:26 2012 +0800
runtime: use sched_getaffinity for runtime.NumCPU() on Linux
Fixes #3921.
R=iant
CC=golang-dev
https://golang.org/cl/6448132
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4f308edc864a87ff4f6f9d6c531733cb1bf100f1
元コミット内容
このコミットは、GoランタイムがLinux上でruntime.NumCPU()
を実装する際に、sched_getaffinity
システムコールを使用するように変更します。これにより、Issue #3921で報告された問題が修正されます。
変更の背景
Goのruntime.NumCPU()
関数は、Goプログラムが利用可能な論理CPUコアの数を報告します。これは、Goのスケジューラが並行処理を最適化するために重要な情報です。このコミット以前は、Linuxシステムにおいて、runtime.NumCPU()
は/proc/stat
ファイルを読み込み、その内容をパースしてCPU数を特定していました。
しかし、この/proc/stat
をパースする方法にはいくつかの問題がありました。
- 信頼性の問題:
/proc/stat
のフォーマットは、将来のLinuxカーネルの変更によって変わる可能性があり、Goランタイムがその変更に追従できない場合、誤ったCPU数を報告するリスクがありました。 - 正確性の問題:
/proc/stat
はシステム全体のCPU使用状況を報告しますが、プロセスに設定されたCPUアフィニティマスク(プロセスが実行を許可されているCPUのセット)を考慮しません。例えば、特定のCPUにアフィニティが設定されているプロセスが、システム全体のCPU数を報告してしまうと、Goスケジューラが利用できないCPUにゴルーチンをスケジュールしようとする可能性があり、非効率性やパフォーマンスの問題を引き起こす可能性がありました。 - パフォーマンスの問題: ファイルI/Oと文字列パースは、システムコールを直接呼び出すよりもオーバーヘッドが大きい可能性があります。
このコミットが修正するIssue #3921は、まさにこの問題、特にCPUアフィニティが設定されている場合にruntime.NumCPU()
が正しくない値を返すという問題に対処するためのものです。sched_getaffinity
システムコールは、特定のプロセス(または現在のプロセス)が実行を許可されているCPUのビットマスクを直接取得できるため、より正確で堅牢な方法を提供します。
前提知識の解説
1. Goランタイムとスケジューラ
Goは独自のランタイムとスケジューラを持っています。Goのスケジューラは、Goプログラム内で作成された軽量な並行処理単位である「ゴルーチン」を、利用可能なOSスレッド(M: Machine)とCPUコア(P: Processor)に効率的に割り当てて実行します。runtime.NumCPU()
は、このスケジューラが利用可能なCPUリソースを把握し、並行処理の度合いを決定するための重要なヒントとなります。
2. CPUアフィニティ (CPU Affinity)
CPUアフィニティとは、プロセスやスレッドが特定のCPUコアまたはCPUコアのセット上で実行されるように制限する機能です。これは、キャッシュの局所性を高めたり、リアルタイムアプリケーションの応答性を向上させたりするために使用されます。Linuxでは、sched_setaffinity
システムコールでアフィニティを設定し、sched_getaffinity
システムコールで現在のアフィニティ設定を取得できます。
3. /proc/stat
ファイル
/proc/stat
はLinuxカーネルが提供する仮想ファイルシステム(procfs)の一部で、システム全体のCPU使用状況、ディスクI/O、ページングなどの統計情報を提供します。CPUに関する情報は、cpu
で始まる行に記載されており、各論理CPUコアの統計情報がcpu0
, cpu1
などの行で提供されます。
4. sched_getaffinity
システムコール
sched_getaffinity
はLinuxのシステムコールで、指定されたプロセス(または現在のプロセス)のCPUアフィニティマスクを取得します。このマスクはビットマップ形式で、各ビットが対応するCPUコアの利用可能性を示します。例えば、0番目のビットがセットされていればCPU0が利用可能であることを意味します。
5. VDSO (Virtual Dynamic Shared Object)
VDSOは、Linuxカーネルがユーザー空間のプロセスに提供する特別な共有ライブラリです。これにより、一部のシステムコール(例: gettimeofday
, clock_gettime
, getcpu
)は、カーネルモードへのコンテキストスイッチなしでユーザー空間から直接実行できるようになります。これは、これらのシステムコールの呼び出し頻度が高く、パフォーマンスがクリティカルであるためです。sched_getaffinity
もVDSO経由で呼び出されることがあります。
6. アセンブリ言語 (x86, AMD64, ARM)
Goランタイムは、パフォーマンスが重要な部分や、OSの低レベルな機能(システムコールなど)と直接やり取りする部分でアセンブリ言語を使用します。このコミットでは、sched_getaffinity
システムコールを呼び出すためのラッパー関数が、386 (x86), AMD64 (x86-64), およびARMアーキテクチャ向けにアセンブリで実装されています。
技術的詳細
このコミットの核心は、runtime.NumCPU()
の実装を、/proc/stat
のパースからsched_getaffinity
システムコールの呼び出しに切り替えることです。
-
アセンブリラッパーの追加:
src/pkg/runtime/sys_linux_386.s
src/pkg/runtime/sys_linux_amd64.s
src/pkg/runtime/sys_linux_arm.s
これらのファイルに、runtime·sched_getaffinity
という新しいアセンブリ関数が追加されました。この関数は、各アーキテクチャのABI(Application Binary Interface)に従ってレジスタに引数をセットし、対応するsched_getaffinity
システムコール番号(386/ARMでは242
、AMD64では204
)をAX
レジスタ(またはARMのR7
)にロードして、システムコールを実行します。- 386版とARM版はVDSO経由でシステムコールを呼び出しています(
CALL *runtime·_vdso(SB)
またはSWI $0
)。 - AMD64版は直接
SYSCALL
命令を使用しています。
-
thread_linux.c
のgetproccount
関数の変更:getproccount
関数は、Goのruntime.NumCPU()
が内部的に呼び出す関数です。- 変更前: この関数は
/proc/stat
ファイルを開き、その内容を読み込み、\ncpu
という文字列を検索してCPU数をカウントしていました。これは、ファイルI/Oと文字列検索を伴うため、効率が悪く、またCPUアフィニティを考慮しないという問題がありました。 - 変更後:
runtime·open
,runtime·read
,runtime·close
,runtime·strstr
,runtime·memmove
といったファイルI/Oおよび文字列操作のコードが削除されました。- 代わりに、
extern runtime·sched_getaffinity(uintptr pid, uintptr len, uintptr *buf);
という宣言が追加され、新しく追加されたアセンブリラッパー関数を呼び出せるようになりました。 runtime·sched_getaffinity(0, sizeof(buf), buf);
を呼び出します。pid=0
は現在のプロセスを意味します。sizeof(buf)
は、CPUアフィニティマスクを格納するバッファのサイズを渡します。buf
はuintptr buf[16]
として定義されており、これは最大で16 * sizeof(uintptr) * 8
ビット(例えば64bitシステムなら1024CPU、32bitシステムなら512CPU)のアフィニティマスクを扱えることを意味します。buf
はアフィニティマスクが書き込まれるバッファです。
- システムコールから返されたアフィニティマスク(
buf
に格納されている)のセットされているビット数をカウントします。このビットカウントは、以下の高速なビット操作アルゴリズム(Population Count)を使用して行われます。
このアルゴリズムは、各バイトのビット数を効率的に合計し、最終的に全体のビット数を算出します。t = buf[i]; t = t - ((t >> 1) & 0x5555555555555555ULL); t = (t & 0x3333333333333333ULL) + ((t >> 2) & 0x3333333333333333ULL); cnt += (int32)((((t + (t >> 4)) & 0xF0F0F0F0F0F0F0FULL) * 0x101010101010101ULL) >> 56);
- 最終的にカウントされたビット数が、利用可能なCPUコア数として返されます。もしカウントが0だった場合は、デフォルトで1を返します(少なくとも1つのCPUは利用可能であるという前提)。
この変更により、runtime.NumCPU()
は、プロセスに設定されたCPUアフィニティを正確に反映したCPU数を報告できるようになり、Goスケジューラがより効率的に動作するようになります。
コアとなるコードの変更箇所
src/pkg/runtime/sys_linux_386.s
+TEXT runtime·sched_getaffinity(SB),7,$0
+ MOVL $242, AX // syscall - sched_getaffinity
+ MOVL 4(SP), BX
+ MOVL 8(SP), CX
+ MOVL 12(SP), DX
+ CALL *runtime·_vdso(SB)
+ RET
src/pkg/runtime/sys_linux_amd64.s
+TEXT runtime·sched_getaffinity(SB),7,$0
+ MOVQ 8(SP), DI
+ MOVL 16(SP), SI
+ MOVQ 24(SP), DX
+ MOVL $204, AX // syscall entry
+ SYSCALL
+ RET
src/pkg/runtime/sys_linux_arm.s
+#define SYS_sched_getaffinity (SYS_BASE + 242)
...
+TEXT runtime·sched_getaffinity(SB),7,$0
+ MOVW 0(FP), R0
+ MOVW 4(FP), R1
+ MOVW 8(FP), R2
+ MOVW $SYS_sched_getaffinity, R7
+ SWI $0
+ RET
src/pkg/runtime/thread_linux.c
-int32 fd, rd, cnt, cpustrlen;
-byte *cpustr, *pos, *bufpos;
-byte buf[256];
+uintptr buf[16], t;
+int32 r, cnt, i;
-fd = runtime·open((byte*)"/proc/stat", O_RDONLY|O_CLOEXEC, 0);
-if(fd == -1)
- return 1;
cnt = 0;
-bufpos = buf;
-cpustr = (byte*)"\ncpu";
-cpustrlen = runtime·findnull(cpustr);
-for(;;) {
- rd = runtime·read(fd, bufpos, sizeof(buf)-cpustrlen);
- if(rd == -1)
- break;
- bufpos[rd] = 0;
- for(pos=buf; pos=runtime·strstr(pos, cpustr); cnt++, pos++) {
- }
- if(rd < cpustrlen)
- break;
- runtime·memmove(buf, bufpos+rd-cpustrlen+1, cpustrlen-1);
- bufpos = buf+cpustrlen-1;
-}
-runtime·close(fd);
+r = runtime·sched_getaffinity(0, sizeof(buf), buf);
+if(r > 0)
+for(i = 0; i < r/sizeof(buf[0]); i++) {
+ t = buf[i];
+ t = t - ((t >> 1) & 0x5555555555555555ULL);
+ t = (t & 0x3333333333333333ULL) + ((t >> 2) & 0x3333333333333333ULL);
+ cnt += (int32)((((t + (t >> 4)) & 0xF0F0F0F0F0F0F0FULL) * 0x101010101010101ULL) >> 56);
+}
+
return cnt ? cnt : 1;
コアとなるコードの解説
アセンブリコード (sys_linux_*.s
)
各アーキテクチャのアセンブリファイルに追加されたruntime·sched_getaffinity
関数は、GoランタイムがCコードからsched_getaffinity
システムコールを呼び出すための低レベルなインターフェースを提供します。
TEXT runtime·sched_getaffinity(SB),7,$0
: Goのアセンブリ構文で関数を定義しています。SB
はStatic Baseで、グローバルシンボルを示します。7
はスタックフレームのサイズ、$0
は引数のサイズです。- レジスタへの引数セット:
sched_getaffinity
システムコールは、pid_t pid
,size_t cpusetsize
,cpu_set_t *mask
の3つの引数を取ります。- 386 (x86): 引数はスタックにプッシュされ、システムコール呼び出し時に
BX
,CX
,DX
レジスタにそれぞれpid
,cpusetsize
,mask
がロードされます。システムコール番号はAX
レジスタに242
がセットされます。 - AMD64 (x86-64): 引数は呼び出し規約(System V AMD64 ABI)に従って
DI
,SI
,DX
レジスタにそれぞれpid
,cpusetsize
,mask
がロードされます。システムコール番号はAX
レジスタに204
がセットされます。 - ARM: 引数は
R0
,R1
,R2
レジスタにそれぞれpid
,cpusetsize
,mask
がロードされます。システムコール番号はR7
レジスタに242
がセットされます。
- 386 (x86): 引数はスタックにプッシュされ、システムコール呼び出し時に
- システムコール呼び出し:
- 386とARMでは
CALL *runtime·_vdso(SB)
またはSWI $0
を使用しており、これはVDSO経由またはソフトウェア割り込みによるシステムコール呼び出しを示します。 - AMD64では
SYSCALL
命令を直接使用しています。
- 386とARMでは
RET
: 関数から戻ります。システムコールの戻り値は通常、AX
レジスタ(またはARMのR0
)に格納されます。
Cコード (thread_linux.c
のgetproccount
関数)
getproccount
関数は、Goランタイムが利用可能なCPU数を取得するロジックをカプセル化しています。
uintptr buf[16], t;
:buf
はsched_getaffinity
から返されるCPUアフィニティマスクを格納するための配列です。uintptr
はポインタサイズに合わせた整数型で、システムが32bitか64bitかによってサイズが変わります。16
要素は、最大で16 * sizeof(uintptr) * 8
個のCPUをサポートできることを意味します。t
はビットカウントのための一時変数です。int32 r, cnt, i;
:r
はsched_getaffinity
の戻り値(通常は成功時に0、エラー時に-1)、cnt
は最終的なCPUカウント、i
はループカウンタです。r = runtime·sched_getaffinity(0, sizeof(buf), buf);
:pid=0
は現在のプロセスのアフィニティマスクを取得することを意味します。sizeof(buf)
は、buf
配列のバイトサイズを渡します。これはsched_getaffinity
が書き込むことができる最大バイト数を示します。buf
は、取得したCPUアフィニティマスクが書き込まれるメモリ領域へのポインタです。
if(r > 0)
:sched_getaffinity
が成功し、有効なデータが返された場合にのみ処理を続行します。r
は通常0を返しますが、ここではr > 0
という条件で、実際に書き込まれたバイト数を確認している可能性があります(ただし、一般的なsched_getaffinity
の戻り値の解釈とは少し異なります。Goランタイムの内部的な規約かもしれません)。- ビットカウントループ:
for(i = 0; i < r/sizeof(buf[0]); i++)
:buf
配列の各uintptr
要素をループ処理します。r/sizeof(buf[0])
は、sched_getaffinity
が書き込んだuintptr
の要素数を示します。t = buf[i];
: 現在のuintptr
要素をt
にコピーします。- Population Count (ビットカウント) アルゴリズム:
これは、SWAR (SIMD Within A Register)テクニックを用いた高速なビットカウントアルゴリズムです。各ステップで、ビットのペア、クワッド、バイトなどのグループ内のセットビット数を合計していきます。最終的に、t = t - ((t >> 1) & 0x5555555555555555ULL); t = (t & 0x3333333333333333ULL) + ((t >> 2) & 0x3333333333333333ULL); cnt += (int32)((((t + (t >> 4)) & 0xF0F0F0F0F0F0F0FULL) * 0x101010101010101ULL) >> 56);
cnt
にbuf
内の全セットビットの合計が加算されます。この合計が、利用可能なCPUコアの数となります。
return cnt ? cnt : 1;
: 計算されたCPU数cnt
を返します。もしcnt
が0だった場合(例えば、sched_getaffinity
が失敗したか、アフィニティマスクが空だった場合)、少なくとも1つのCPUは利用可能であるという前提で1
を返します。
この変更により、GoランタイムはLinuxシステム上でより正確かつ効率的に利用可能なCPUリソースを特定できるようになり、Goプログラムのパフォーマンスとリソース利用の最適化に貢献します。
関連リンク
- Go Issue #3921: https://github.com/golang/go/issues/3921
- Go Gerrit Change 6448132: https://golang.org/cl/6448132
参考にした情報源リンク
sched_getaffinity(2)
man page: https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html/proc/stat
man page: https://man7.org/linux/man-pages/man5/proc.5.html- Population Count (Wikipedia): https://en.wikipedia.org/wiki/Population_count
- VDSO (Virtual Dynamic Shared Object) (Wikipedia): https://en.wikipedia.org/wiki/VDSO
- Go Assembly Language (Go Wiki): https://go.dev/doc/asm
- System V Application Binary Interface AMD64 Architecture Processor Supplement: https://refspecs.linuxfoundation.org/elf/x86-64-abi-0.99.pdf