CPUコア数を数えるAPI
Linuxでプロセッサ数をカウントするための方法として sysconf(_SC_NPROCESSORS_ONLN)
と sched_getaffinity
を用いる方法がある。(厳密にいうとこれらのAPIはいずれもhyper threadingをコアと認識するため、そうでない場合を考える必要があるが今回は考慮しない)
この2つは異なる挙動をする。sysconfのほうはOSが認識しているプロセッサ数を返し、sched_getaffinityはプロセスが利用可能なプロセッサの数を返す。
各言語がどのようにAPIを提供しているか調べてみるとおもしろかったので、まとめてみる。
以下の各言語のサンプル出力を行うプログラムは https://github.com/kubo39/cpucount にある。
C++11/14
-
http://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency
number of concurrent threads supported. If the value is not well defined or not computable, returns 0.
$ g++ --version | head -1
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
$ g++ -std=c++14 cpp/cpucount.cpp
$ ./a.out
4
$ taskset -c 0,1 ./a.out
4
StackOverflowでみつけた回答から hardware_concurrency
を選択した。
number of concurrent threads supported ~
であるならばプロセス単位の利用可能なCPU数が返ってくると思ったが、そうではないようだ。
get_nprocs(3)
が使える場合は優先して使う。これは sysconf( _ SC_NPROCESSORS_ONLN)
と等価な結果になる。失敗した場合は0を返す。
なんともわかりにくい設計だとおもう。
Python
-
https://docs.python.org/3.6/library/os.html#os.cpu_count
Return the number of CPUs in the system. Returns None if undetermined.
This number is not equivalent to the number of CPUs the current process can use.
The number of usable CPUs can be obtained with len(os.sched_getaffinity(0))
$ python --version
Python 3.6.1
$ python python/cpucount.py
4
4
$ taskset -c 0,1 python python/cpucount.py
4
2
PythonはAPIを分けている。os.cpu_count
はsysconfを使い、 os.sched_getaffinity(0)
は名前の通り同名のC関数を用いる。Rubyと違いfallbackさせる仕組みはない。失敗した場合はNoneを返す。
Ruby
-
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/65347
-
https://ruby-doc.org/stdlib-2.2.0/libdoc/etc/rdoc/Etc.html#method-c-nprocessors
Returns the number of online processors.
The result is intended as the number of processes to use all available processors.
This method is implemented using:- sched_getaffinity(): Linux
- sysconf(_SC_NPROCESSORS_ONLN): GNU/Linux, NetBSD, FreeBSD, OpenBSD, DragonFly BSD, OpenIndiana, Mac OS X, AIX
The result might be smaller number than physical cpus especially when ruby process is bound to specific cpus.
This is intended for getting better parallel processing.
$ ruby --version
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]
$ ruby ruby/cpucount.rb
4
$ taskset -c 0,1 ruby ruby/cpucount.rb
2
Etc.nprocessorsはRuby2.2から新設されている。
コンテナ環境での利用を想定してプロセス単位で利用可能なCPU数を優先して返すように設計されている。
sched_getaffinity
に失敗した場合は sysconf(_SC_NPROCESSORS_ONLN)
にfallbackする。
sysconfでエラーになった場合例外を投げる。
Go
-
https://golang.org/pkg/runtime/#NumCPU
NumCPU returns the number of logical CPUs usable by the current process.
The set of available CPUs is checked by querying the operating system at process startup.
Changes to operating system CPU allocation after process startup are not reflected.
$ go version
go version go1.6.2 linux/amd64
$ go run golang/cpucount.go
4
$ taskset -c 0,1 go run golang/cpucount.go
2
number of logical CPUs usable by the current process とあるとおり、プロセスが利用可能な論理コアをセットする。runtimeの起動時に sched_getaffinity
で取得した値をセットする。失敗した場合は1を返す。
Rust
-
https://github.com/seanmonstar/num_cpus
Count the number of CPUs on the current machine.
$ rustc --version # num_cpus crate = "1.4.0"
rustc 1.19.0-nightly (5b13bff52 2017-05-23)
$ cargo run -q
4
$ taskset -c 0,1 cargo run -q
4
現在は sysconf(_SC_NPROCESSORS_ONLN)
を返すのでマシンのプロセス数を返すが、 sched_getaffinity
を使う修正がとりこまれた。Rubyと同様 sysconf(_SC_NPROCESSORS_ONLN)
にfallbackするようになってるけど、あまり考えていなかったので後で修正するかも。
D
-
https://dlang.org/phobos/std_parallelism.html#.totalCPUs
The total number of CPU cores available on the current machine, as reported by the operating system.
$ dmd --version
DMD64 D Compiler v2.074.0
Copyright (c) 1999-2017 by Digital Mars written by Walter Bright
$ rdmd dlang/cpucount.d
4
$ taskset -c 0,1 rdmd dlang/cpucount.d
4
APIとしては、 total number of CPU cores available on the current machine
として正しい挙動をしている。
しかし、用途で考えるのであれば、std.parallelismというSMP並列なプログラムを書くためのモジュールとしては好ましくないと思われる。
また、Dはモジュールimport時に一度だけtotalCPUsを定数としてセットするのだが、一切エラーハンドルをしていないのも特徴である。
こちらはそのうちbugzillaをたてるかも(すでにあるかもしれない)
Nim
-
https://nim-lang.org/docs/osproc.html#countProcessors
returns the numer of the processors/cores the machine has.
Returns 0 if it cannot be detected.
$ nim --help| head -1
Nim Compiler Version 0.17.0 (2017-05-17) [Linux: amd64]
$ nim c -r nim/cpucount.nim
...
4
$ taskset -c 0,1 nim c -r nim/cpucount.nim
...
4
マシンがもっているプロセッサの数を返すので、この挙動は説明通りであるのだが、まあなんとも潔い。失敗した場合は0を返すとあるが、実行に使ったタイミングでのNimの実装では1を返すようになっておりドキュメントと一致していない。
概観
いくつか傾向がみえてくる。
エラーの扱い
- 0を返すよ派 (例: C++,Nim?)
- 1を返すよ派(例: Go, Rust)
- 例外を投げるよ派(例: Ruby)
- Noneを返すよ派(例: Python)
- そもそもエラーハンドリングしないよ(例: D)
そもそも sysconf(_SC_NPROCESSORS_ONLN)
は失敗する場合があるのだろうか。manには制限が設定されていない場合に -1 が返されるとある。
Rustは1を返すよ派に入れてるけど、Optionにしたほうがいいか悩んでる。
sysconfへのfallback
fallbackする実装はRubyと(現在のところ)Rustのみ。
実装
GolangとRubyの実装が興味深い。同じ問題に対する解決策なのだけれど、解決方法が異なっている。
Golang
ソースコードはこちら。
func getproccount() int32 {
// This buffer is huge (8 kB) but we are on the system stack
// and there should be plenty of space (64 kB).
// Also this is a leaf, so we're not holding up the memory for long.
// See golang.org/issue/11823.
// The suggested behavior here is to keep trying with ever-larger
// buffers, but we don't have a dynamic memory allocator at the
// moment, so that's a bit tricky and seems like overkill.
const maxCPUs = 64 * 1024
var buf [maxCPUs / (sys.PtrSize * 8)]uintptr
r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
if r < 0 {
return 1
}
n := int32(0)
for _, v := range buf[:r/sys.PtrSize] {
for v != 0 {
n += int32(v & 1)
v >>= 1
}
}
if n == 0 {
n = 1
}
return n
}
アルゴリズムは、
- まずaffinityマスクを書き込むためのバッファを用意する。バッファは(amd64と仮定すると) 要素
(64 * 1024) / (8 * 8) = 1024
のuintptrの配列で8kBを確保する。 -
sched_getaffinity
関数を呼び出す。golang内部のsched_getaffinity
関数が返す値はGlibcと違い成功したら書き込みバイト数が返る。 - 内部で
sched_getaffinity(2)
の呼び出しに失敗した場合は1を返して関数から抜ける。 - affinityマスクがセットされたバッファをチェックし、ビットが立っている場合はカウンタに1を加算する。
- それでもみつからない場合に1を返す。
golangはまず巨大なバッファを割り当ててしまう方式。
Ruby
ソースコードはこちら。
コメントをみると、
- CPU_ALLOCはCPU数を保持するのに十分なバッファを提供するとあるが、これは正しくない
- カーネル内部のbitmap以上のサイズを割り当てようとするとsched_getaffinity(2)はEINVALを返す
- /sys/devices/system/cpu/onlineを用いるべきだが、コストが高いし存在する保証もない(コンテナなど)
- けっきょくハードコードすることにした
- Linux3.17は最大8192個までしかサポートしないので、16384まで考慮すれば十分だろう
#if defined(HAVE_SCHED_GETAFFINITY) && defined(CPU_ALLOC)
static int
etc_nprocessors_affin(void)
{
cpu_set_t *cpuset;
size_t size;
int ret;
int n;
/*
* XXX:
* man page says CPU_ALLOC takes number of cpus. But it is not accurate
* explanation. sched_getaffinity() returns EINVAL if cpuset bitmap is
* smaller than kernel internal bitmap.
* That said, sched_getaffinity() can fail when a kernel have sparse bitmap
* even if cpuset bitmap is larger than number of cpus.
* The precious way is to use /sys/devices/system/cpu/online. But there are
* two problems,
* - Costly calculation
* It is a minor issue, but possibly kill a benefit of a parallel processing.
* - No guarantee to exist /sys/devices/system/cpu/online
* This is an issue especially when using Linux containers.
* So, we use hardcode number for a workaround. Current linux kernel
* (Linux 3.17) support 8192 cpus at maximum. Then 16384 must be enough.
*/
for (n=64; n <= 16384; n *= 2) {
size = CPU_ALLOC_SIZE(n);
if (size >= 1024) {
cpuset = xcalloc(1, size);
if (!cpuset)
return -1;
} else {
cpuset = alloca(size);
CPU_ZERO_S(size, cpuset);
}
ret = sched_getaffinity(0, size, cpuset);
if (ret == 0) {
/* On success, count number of cpus. */
ret = CPU_COUNT_S(size, cpuset);
}
if (size >= 1024) {
xfree(cpuset);
}
if (ret > 0) {
return ret;
}
}
return ret;
}
#endif
アルゴリズムは、
- 64 -> 128 -> ... -> 16384(上で説明した閾値)まで倍々でループをまわす。以下はその中の処理
- affinityマスク書き込むようのバッファを用意
- sched_getaffinity(2)を呼ぶ
- 成功してかつカウントが0以上だった場合はその値を返して関数を抜ける
- そうでない場合は次のループへ
- みつからなかったら0を返す(sysconf(3)へfallback)
Rubyはstep by stepで徐々に大きくしていく。
ソースコードライセンス
引用だといらないらしいけど、念のため。
- Golang
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- Ruby
Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
You can redistribute it and/or modify it under either the terms of the
2-clause BSDL (see the file BSDL), or the conditions below:
1. You may make and give away verbatim copies of the source form of the
software without restriction, provided that you duplicate all of the
original copyright notices and associated disclaimers.
2. You may modify your copy of the software in any way, provided that
you do at least ONE of the following:
a) place your modifications in the Public Domain or otherwise
make them Freely Available, such as by posting said
modifications to Usenet or an equivalent medium, or by allowing
the author to include your modifications in the software.
b) use the modified software only within your corporation or
organization.
c) give non-standard binaries non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
3. You may distribute the software in object code or binary form,
provided that you do at least ONE of the following:
a) distribute the binaries and library files of the software,
together with instructions (in the manual page or equivalent)
on where to get the original distribution.
b) accompany the distribution with the machine-readable source of
the software.
c) give non-standard binaries non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
4. You may modify and include the part of the software into any other
software (possibly commercial). But some files in the distribution
are not written by the author, so that they are not under these terms.
For the list of those files and their copying conditions, see the
file LEGAL.
5. The scripts and library files supplied as input to or produced as
output from the software do not automatically fall under the
copyright of the software, but belong to whomever generated them,
and may be sold commercially, and may be aggregated with this
software.
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.