Socionext SC0FQAA-BはNUMAか否か

はじめに

本記事はLinuxのプロセススケジューラから見たSocionext SC2A11の続きです。Socionext SC0FQAA-B(以下「本マシン」と記載)は24コアのSC2A11を1つ搭載しています。このマシンがNUMAかどうか、言い方を変えるとSC2A11内の各コアがすべてのメモリに等速でアクセス可能かどうかを調査しました。

結論

本マシンはNUMAではないと推測しました。

linuxからの情報取得

まず、arm64はNUMA構成をサポートしています。LinuxのDevice Treeのドキュメントによると、Device TreeでNUMAを表現するには、各デバイスの定義内のnuma-node-idというトークンによってデバイスが所属するNUMA IDを定義します。

しかし、SC0FQAA-BのDevice Treeにはこのトークンは見つかりませんでした。

$ find /sys/firmware/devicetree -name numa-node-id
$

ここで終わりかというとそうではなくて、ハードがNUMAとして見せていないにも関わらず、実測してみると実質NUMAだと明らかになる、というのは珍しい話ではありません。

実験

使用するプログラム

データ採取には次のような仕様のプログラムを使います。

  1. コア0上でL3キャッシュを超えるサイズのバッファ(ここでは16MB)を取得する
  2. 以下を24コアのすべてにおいて繰り返す。コア番号をnとする
  3. 2-1. プロセスをコアnに移動させる
  4. 2-2. 上記バッファに所定の回数アクセスすることによってメモリアクセス速度を計測

処理1において取得したメモリはプログラムの実行中に物理アドレスが移動することはないため1、処理2-1においてコアを移動した場合にバッファへアクセス速度が変化すれば、おそらく本マシンはNUMAだろう、というわけです。

これを実装したのが次のnuma.cプログラムです。

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

#define CACHE_LINE_SIZE 64
#define NLOOP           (64*1024*1024)
#define BUFFER_SIZE     (16*1024*1024)
#define NCORE           24

#define NSECS_PER_SEC   1000000000UL

static inline long diff_nsec(struct timespec before, struct timespec after)
{
        return ((after.tv_sec * NSECS_PER_SEC + after.tv_nsec)
                - (before.tv_sec * NSECS_PER_SEC + before.tv_nsec));
}

static void setcpu(int n)
{
        cpu_set_t set;
        CPU_ZERO(&set);
        CPU_SET(n, &set);
        if (sched_setaffinity(getpid(), sizeof(cpu_set_t), &set) == -1)
                err(EXIT_FAILURE, "sched_setaffinity() failed");
}

int main(int argc, char *argv[])
{
        char *progname;
        progname = argv[0];

        register int size = BUFFER_SIZE;

        setcpu(0);
        char *buffer;
        buffer = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (buffer == (void *) -1)
                err(EXIT_FAILURE, "mmap() failed");

        int i;
        for (i = 0; i < NCORE; i++) {
                setcpu(i);

                struct timespec before, after;
                clock_gettime(CLOCK_MONOTONIC, &before);
                int j;
                for (j = 0; j < NLOOP / (size / CACHE_LINE_SIZE); j++) {
                        long k;
                        for (k = 0; k < size; k += CACHE_LINE_SIZE)
                                buffer[k] = 0;
                }
                clock_gettime(CLOCK_MONOTONIC, &after);
                printf("%d\t%f\n", i, (double)diff_nsec(before, after) / NLOOP);
        }





        if (munmap(buffer, size) == -1)
                err(EXIT_FAILURE, "munmap() failed");
        exit(EXIT_SUCCESS);
}

結果

結果は次の通りです。第一フィールドがデータを採取したコア、第二フィールドがメモリアクセス処理に所要時間です。

0       37.300696
1       37.511301
2       37.320645
3       37.479546
4       37.280271
5       37.298240
6       37.207461
7       37.285339
8       36.824337
9       36.862199
10      36.926653
11      36.867224
12      37.064979
13      37.011814
14      37.041438
15      37.074635
16      37.414566
17      37.373740
18      37.375946
19      37.423050
20      37.292513
21      37.323017
22      37.154486
23      37.190995

次に示すのは、コア0上でバッファにアクセスする測定を24回繰り返した結果です。

36.993787
36.986646
37.254620
37.231557
37.587849
37.582042
37.450369
37.387533
37.487312
37.452216
37.501526
37.556768
37.139728
37.283043
37.169193
37.389888
37.746065
37.841875
37.559928
37.581090
37.769728
37.680080
37.567870
37.194819

いずれも37秒前後1秒程度の差に収まっているため、アクセス速度にあまり違いはないといっていいでしょう。つまり、ある物理メモリ(numa.cにおいて取得したバッファ)には24個いずれのコアからもおおよそ等速でアクセスできたということです。numa.cの処理1においてバッファを採取する際のコアを1-23に変えても結果は同じでした。これによって本マシンは恐らくNUMAではなかろうと推測しました。


  1. メモリが断片化してきた場合はコンパクションという機能によって物理メモリが移動することがありますが、今回はほとんどのメモリが空いている状態なのでその心配はありません。