0%

benchmark 访存延迟和带宽测量

Reference:

  1. Cache 技术实战分析
  2. Performance Analysis and Tuning on Modern CPU, 4.10 内存延迟和带宽

测量方法:

  1. x86 平台可用 Intel 提供的内存延迟检查器 MLC
  2. 已有的 Benchmark 基准测试程序:
    1. lmbench
    2. bandwidth
    3. Stream
  3. 自制简单 benchmark 程序

Intel MLC

  • MLC 可以使用不同的访问模式和负载来测量缓存和内存的延迟和带宽

延迟测量

  • MLC 通过进行相关加载(也称为指针追 踪)来测量空闲延迟。一个测量线程分配一个非常大的缓冲区,并对其进行初始化,以便缓冲区内的每个(64 字节) 缓存行包含指向该缓冲区内另一个非相邻缓存行的指针。通过适当调整缓冲区的大小,我们可以确保几乎所有的加 载都命中某个级别的缓存或主存。

简易使用:

1
./mlc --idle_latency -c0 -L -b10m
  • --idle_latency: 测量读取延迟而不加载系统
    • 而另一个 --loaded_latency 选项, 用于在由其他线程生成的内存流量存在时测量延迟
  • -c0 将测量线程固定在逻辑 CPU 0 上
  • -L 启用大页以限制测量中的 TLB 影响
    • 如果操作系统中没有足够的大页,可以不使用该选项,但 TLB 影响就无法避免
  • -b10m 告诉 MLC 使用 10MB 缓冲区
    • 使用不同的缓冲区大小扫描可以得知系统的多级 cache 访问延时和不同级别的 cache 大小

带宽测量

MLC 使用不同的读写比例来测量带宽

1
./mlc --max_bandwidth -k0-15 -Y -L -b10m
  • -k 指定了用于测量的 CPU 编号列表
  • -Y 选项告诉 MLC 使用 AVX2 加载, 即每次加载 32 字节

自制简单 Benchmark 程序

访存读取延时测量一般会分配一个较大的 buffer ,遍历整个 buffer 以使 cache 被填满从而触发 cache miss 并访问下一级 cache。
采用指针追踪的方式(可以避免编译器优化掉访存指令),即访问 buffer 的每一项中都保存着下一次访问的 buffer 的索引,下一次的访问将以当前 buffer 中读出之后的数据作为地址继续访问。

代码实现框架包含两步:
申请一个 buffer,buffer size 需要大于 upper level cache size,小于等于当前 level cache size

  1. 第一次遍历进行预热,将数据全部加载到 cache 中
    • 数据就是下一次访问的地址 (指针追逐)
  2. 第二次遍历统计耗时,计算每次 read 的延迟平均值
    • 通过宏展开使得编译多条实际的访存指令,避免分支指令和循环计算指令的执行对于访存延时的统计产生影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
///==============================
/// loop1: warmup the mem
///==============================
// fill mem with pointer references
// let the each accessed item be the pointer to the next accessed item.
for (i = 0; i < size - 1; i++)
*(char **)&mem[indices[i] * stride] = (char*)&mem[indices[i + 1] * stride];
*(char **)&mem[indices[size-1]*stride] = (char*)&mem[indices[0]*stride];

gettimeofday(&tv1, &tz);

register char **p = (char **) mem;
///==============================
/// loop2: get the delay.
///==============================
for (i = 0; i < count / 100; ++i) {
HUNDRED; // 100 times: p = (char **)*p;
}

gettimeofday(&tv2, &tz);

访存步幅的影响

访存步幅如果以 8 字节为粒度,顺序读取 128KB 的 buffer,假设数据命中的是 L2,那么数据就会被缓存到 L1,一个 cache line 其他的访存操作都只会命中 L1,从而导致测量的 L2 延迟明显偏小。
因此访存步幅应该以 cache line size 为粒度

硬件预取的影响

  1. 通过简单地将访存步幅再提高一点,可以避免一部分预取的影响
  2. 另一种更为通用的做法是保存指针时随机化(并限制到 buffer 内)
1
2
3
4
5
6
7
8
9
10
11
for (i = 0; i < size; i++)
indices[i] = i;
// shuffle indices: to avoid prefetch.
for (i = 0; i < size; i++) {
j = i + rand() % (size - i);
if (i != j) {
tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
}

TLB miss 的影响

如果 buffer size 较大时可能导致 TLB miss 对延时也造成一定影响,可以通过使用大页来减轻影响。

1
2
3
int mem_flag = MAP_PRIVATE | MAP_ANON | MAP_HUGETLB;
char* mem = mmap(NULL, memsize,
PROT_READ | PROT_WRITE, mem_flag, -1, 0);