测量存储延迟时的 C 微基准测试 'bug'

C microbenchmark 'bug' when measuring store latency

我一直在 x86 上尝试一些实验 - 即 mfence 对 store/load 延迟等的影响

这是我的开头:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#define ARRAY_SIZE 10
#define DUMMY_LOOP_CNT 1000000
int main()
{
    char array[ARRAY_SIZE];

    for (int i =0; i< ARRAY_SIZE; i++)
        array[i] = 'x'; //This is to force the OS to give allocate the array

    asm volatile ("mfence\n");
    for (int i=0;i<DUMMY_LOOP_CNT;i++); //A dummy loop to just warmup the processor

    struct result_tuple{
        uint64_t tsp_start;
        uint64_t tsp_end;
        int offset;
        };

    struct result_tuple* results = calloc(ARRAY_SIZE , sizeof (struct result_tuple));
    for (int i = 0; i< ARRAY_SIZE; i++)
    {
        uint64_t *tsp_start,*tsp_end;
        tsp_start = &results[i].tsp_start;
        tsp_end = &results[i].tsp_end;
        results[i].offset = i;

        
        asm volatile (
        "mfence\n"
        "rdtscp\n"
        "mov %%rdx,%[arg]\n"
        "shl ,%[arg]\n"
        "or %%rax,%[arg]\n"
        :[arg]"=&r"(*tsp_start)
        ::"rax","rdx","rcx","memory"
        );

        array[i] = 'y'; //A simple store

        asm volatile (
        "mfence\n"
        "rdtscp\n"
        "mov %%rdx,%[arg]\n"
        "shl ,%[arg]\n"
        "or %%rax,%[arg]\n"
        :[arg]"=&r"(*tsp_end)
        ::"rax","rdx","rcx","memory"
        );
    }

    
    printf("Offset\tLatency\n");
    for (int i=0;i<ARRAY_SIZE;i++)
    {
        printf("%d\t%lu\n",results[i].offset,results[i].tsp_end - results[i].tsp_start);
    }
    free (results);
}   

我用 gcc microbenchmark.c -o microbenchmark 编译很简单 我的系统配置如下:

CPU : Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
Operating system : GNU/Linux (Linux 5.4.80-2)

我的问题是:

例如:

In run 1 I get:
Offset  Latency
1   275
2   262
3   262
4   262
5   275
...
252 275
253 275
254 262
255 262

In another run I get:
Offset  Latency
1   75
2   75
3   75
4   72
5   72
...
251 72
252 72
253 75
254 75
255 72

这非常令人惊讶(among-运行 的变化非常高,而 within-运行 的变化可以忽略不计)!我不知道该如何解释。我的微基准测试有什么问题?

注意:我知道普通存储将是 write allocate 存储。从技术上讲,我的测量是负载(而不是存储)。此外,mfence 应该刷新存储缓冲区,从而确保没有存储 'delayed'.

您的预热虚拟循环仅执行 100 万次迭代,在 -O0 调试版本中约 600 万个时钟周期 - 可能不够长,无法使 CPU 达到最大涡轮增压,在 Skylake 的硬件 P 状态管理之前 CPU。 ()

RDTSCP 计算固定频率参考周期,而不是核心时钟周期。您的 运行 太短了,所有 运行 到 运行 的变化可能都可以用 CPU 频率的低或高来解释。参见 How to get the CPU cycle count in x86_64 from C++?


此外,此调试 (-O0) 构建将在您的定时区域内执行额外的存储和重新加载,但“幸运的是”results[i].offset = i; 存储加上 mfence 在第一个 rdtscp 确保结果数组在进入定时区域之前在缓存中也是热的。

你的数组很小,而且你只做 1 字节的存储(所以 64 个存储都在同一个缓存行中。)它很可能在你初始化它时仍处于 MESI 修改状态,所以我不会' 期待任何 array[i] = 'y' 商店的 RFO。在定时循环之前涉及的几行堆栈内存已经发生了这种情况。如果您想在不缓存阵列的情况下预先对阵列进行故障处理,则可以每 4k 页触摸一行并保持其他行不变。但是 HW 预取将领先于您的存储,特别是如果您一次只存储 1 个字节,每个存储 2 个慢 mfences,因此再次等待离核内存请求将在定时区域之外。您应该期望数据已经在 L1d 缓存中或至少处于独占状态的 L2,准备好在存储中翻转为已修改。

顺便说一句,拥有一个 offset 成员似乎毫无意义;它可以从数组索引中隐含。例如打印 i 而不是 offset[i]。存储开始和停止绝对 TSC 值也不是很有用。你可以只存储一个 32 位的差异,然后你不需要在你的内联 asm 中移位/或,只需在未使用的 EDX 输出上声明一个 clobber。

另请注意,当涉及 mfence 时,“存储延迟”通常只对实际代码的性能有影响。否则重要的是存储->加载转发,这可以在存储提交到 L1d 缓存之前从存储缓冲区发生。这大约是 6 个周期,如果不立即尝试重新加载,有时会更低。 (它在 Sandybridge 系列上是可变的。)