CPU キャッシュメモリ

May 05, 2018

階層型キャッシュメモリ

近年の 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. 第一引数で定められた量のメモリを確保する
  2. 確保したメモリ領域内に、所定の回数だけシーケンシャルアクセスする
  3. 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, &amp;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, &amp;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 であることに由来する。


 © 2023, Dealing with Ambiguity