测量存储延迟时的 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 个慢 mfence
s,因此再次等待离核内存请求将在定时区域之外。您应该期望数据已经在 L1d 缓存中或至少处于独占状态的 L2,准备好在存储中翻转为已修改。
顺便说一句,拥有一个 offset
成员似乎毫无意义;它可以从数组索引中隐含。例如打印 i
而不是 offset[i]
。存储开始和停止绝对 TSC 值也不是很有用。你可以只存储一个 32 位的差异,然后你不需要在你的内联 asm 中移位/或,只需在未使用的 EDX 输出上声明一个 clobber。
另请注意,当涉及 mfence
时,“存储延迟”通常只对实际代码的性能有影响。否则重要的是存储->加载转发,这可以在存储提交到 L1d 缓存之前从存储缓冲区发生。这大约是 6 个周期,如果不立即尝试重新加载,有时会更低。 (它在 Sandybridge 系列上是可变的。)
我一直在 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 个慢 mfence
s,因此再次等待离核内存请求将在定时区域之外。您应该期望数据已经在 L1d 缓存中或至少处于独占状态的 L2,准备好在存储中翻转为已修改。
顺便说一句,拥有一个 offset
成员似乎毫无意义;它可以从数组索引中隐含。例如打印 i
而不是 offset[i]
。存储开始和停止绝对 TSC 值也不是很有用。你可以只存储一个 32 位的差异,然后你不需要在你的内联 asm 中移位/或,只需在未使用的 EDX 输出上声明一个 clobber。
另请注意,当涉及 mfence
时,“存储延迟”通常只对实际代码的性能有影响。否则重要的是存储->加载转发,这可以在存储提交到 L1d 缓存之前从存储缓冲区发生。这大约是 6 个周期,如果不立即尝试重新加载,有时会更低。 (它在 Sandybridge 系列上是可变的。)