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

[インデックス 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をパースする方法にはいくつかの問題がありました。

  1. 信頼性の問題: /proc/statのフォーマットは、将来のLinuxカーネルの変更によって変わる可能性があり、Goランタイムがその変更に追従できない場合、誤ったCPU数を報告するリスクがありました。
  2. 正確性の問題: /proc/statはシステム全体のCPU使用状況を報告しますが、プロセスに設定されたCPUアフィニティマスク(プロセスが実行を許可されているCPUのセット)を考慮しません。例えば、特定のCPUにアフィニティが設定されているプロセスが、システム全体のCPU数を報告してしまうと、Goスケジューラが利用できないCPUにゴルーチンをスケジュールしようとする可能性があり、非効率性やパフォーマンスの問題を引き起こす可能性がありました。
  3. パフォーマンスの問題: ファイル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システムコールの呼び出しに切り替えることです。

  1. アセンブリラッパーの追加:

    • 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命令を使用しています。
  2. thread_linux.cgetproccount関数の変更:

    • 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アフィニティマスクを格納するバッファのサイズを渡します。bufuintptr 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とARMではCALL *runtime·_vdso(SB)またはSWI $0を使用しており、これはVDSO経由またはソフトウェア割り込みによるシステムコール呼び出しを示します。
    • AMD64ではSYSCALL命令を直接使用しています。
  • RET: 関数から戻ります。システムコールの戻り値は通常、AXレジスタ(またはARMのR0)に格納されます。

Cコード (thread_linux.cgetproccount関数)

getproccount関数は、Goランタイムが利用可能なCPU数を取得するロジックをカプセル化しています。

  • uintptr buf[16], t;: bufsched_getaffinityから返されるCPUアフィニティマスクを格納するための配列です。uintptrはポインタサイズに合わせた整数型で、システムが32bitか64bitかによってサイズが変わります。16要素は、最大で16 * sizeof(uintptr) * 8個のCPUをサポートできることを意味します。tはビットカウントのための一時変数です。
  • int32 r, cnt, i;: rsched_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 (ビットカウント) アルゴリズム:
      t = t - ((t >> 1) & 0x5555555555555555ULL);
      t = (t & 0x3333333333333333ULL) + ((t >> 2) & 0x3333333333333333ULL);
      cnt += (int32)((((t + (t >> 4)) & 0xF0F0F0F0F0F0F0FULL) * 0x101010101010101ULL) >> 56);
      
      これは、SWAR (SIMD Within A Register)テクニックを用いた高速なビットカウントアルゴリズムです。各ステップで、ビットのペア、クワッド、バイトなどのグループ内のセットビット数を合計していきます。最終的に、cntbuf内の全セットビットの合計が加算されます。この合計が、利用可能なCPUコアの数となります。
  • return cnt ? cnt : 1;: 計算されたCPU数cntを返します。もしcntが0だった場合(例えば、sched_getaffinityが失敗したか、アフィニティマスクが空だった場合)、少なくとも1つのCPUは利用可能であるという前提で1を返します。

この変更により、GoランタイムはLinuxシステム上でより正確かつ効率的に利用可能なCPUリソースを特定できるようになり、Goプログラムのパフォーマンスとリソース利用の最適化に貢献します。

関連リンク

参考にした情報源リンク