階層型キャッシュメモリ
近年の x86_64 アーキテクチャの CPU におけるキャッシュメモリは階層構造を取っている。
階層型構造における各キャッシュメモリは L1 L2 L3 などの名前がついており、最もレジスタに近く、容量が少なく、かつ高速なのが L1 キャッシュとなる。番号が増えるにつれ、レジスタから遠くなり、容量が多く、低速になる。
キャッシュメモリの情報は /sys/devices/system/cpu/cpu[i]/cache/index[j]/ 内のファイルの中身を見れば良い。(i は論理 CPU 番号、j はキャッシュの種類)
それぞれのファイルは以下の情報を持つ。
- type: キャッシュするデータの種類。Data はデータのみ、Code はコードのみ、Unified は両方
- sharedcpulist: キャッシュを共有する論理 CPU のリスト
- size: サイズ
- coherencylinesize: キャッシュラインサイズ
論理 CPU 0 における L1d キャッシュ
$ cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
$ cat /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list
0,4
$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
論理 CPU 0 における L3 キャッシュ
$ cat /sys/devices/system/cpu/cpu0/cache/index3/type
Unified
$ cat /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list
0-7
$ cat /sys/devices/system/cpu/cpu0/cache/index3/size
46080K
$ cat /sys/devices/system/cpu/cpu0/cache/index3/coherency_line_size
64
論理 CPU 0 における各キャッシュのサイズ
$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index2/size
256K
$ cat /sys/devices/system/cpu/cpu0/cache/index3/size
46080K
キャッシュメモリによる効果の観測
キャッシュメモリの影響により、プロセスがアクセスするデータサイズに依存し、データアクセス時間がどの程度変化するかを観測する。
具体的には以下のようなコードを用意する。
- 第一引数で定められた量のメモリを確保する
- 確保したメモリ領域内に、所定の回数だけシーケンシャルアクセスする
- 1 アクセスあたりの所要時間を表示
実際のコードは以下。
cache.c
#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 (4*1024UL*1024*1024)
#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));
}
int main(int argc, char *argv[]) {
char *progname;
progname = argv[0];
if (argc != 2) {
fprintf(stderr, "usage: %s <size[KB]>\n", progname);
exit(EXIT_FAILURE);
}
register int size;
size = atoi(argv[1]) * 1024;
if (!size) {
fprintf(stderr, "size should be >= 1: %d\n", size);
exit(EXIT_FAILURE);
}
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.");
struct timespec before, after;
clock_gettime(CLOCK_MONOTONIC, &before);
int i;
for (i = 0; i < NLOOP / (size / CACHE_LINE_SIZE); i++) {
long j;
for (j = 0; j < size; j += CACHE_LINE_SIZE)
buffer[j] = 0;
}
clock_gettime(CLOCK_MONOTONIC, &after);
printf("%d KB: %f\n", atoi(argv[1]), (double)diff_nsec(before, after) / NLOOP);
if (munmap(buffer, size) == -1)
err(EXIT_FAILURE, "munmap() failed.");
exit(EXIT_SUCCESS);
}
キャッシュメモリの影響を見えやすくするために、最適化オプション -O3 を付与してコンパイル、実行。
$ gcc -O3 -o cache cache.c
$ for i in 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536; do ./cache $i ; done
4 KB: 0.494538
8 KB: 0.427239
16 KB: 0.408577
32 KB: 0.385194
64 KB: 2.245113
128 KB: 2.258521
256 KB: 2.763080
512 KB: 3.396828
1024 KB: 3.396729
2048 KB: 3.398891
4096 KB: 3.399059
8192 KB: 3.407652
16384 KB: 3.415289
32768 KB: 3.728436
65536 KB: 8.315909
上記より、64 KB 、512 KB 、65536 KB において速度が大きく変化していることが確認できる。
これは、この環境において L1 L2 L3 キャッシュのサイズがそれぞれ 32 KB 、256 KB 、46080 KB であることに由来する。