Help us understand the problem. What is going on with this article?

CPUコア数を数えるAPI(Linux)

More than 1 year has passed since last update.

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

$ 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

$ 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

$ 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

$ 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

$ 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

$ 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.
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away