drc*_*cxd 3 c optimization assembly cpu-architecture microbenchmark
我已阅读 CSAPP 3e 的第 5 章。我想测试一下书中描述的优化技术是否可以在我的计算机上运行。我编写了以下程序:
#define SIZE (1024)
int main(int argc, char* argv[]) {
int sum = 0;
int* array = malloc(sizeof(int) * SIZE);
unsigned long long before = __rdtsc();
for (int i = 0; i < SIZE; ++i) {
sum += array[i];
}
unsigned long long after = __rdtsc();
double cpe = (double)(after - before) / SIZE;
printf("CPE is %f\n", cpe);
printf("sum is %d\n", sum);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
据报道,CPE 约为 1.00。
我使用 4x4 循环展开技术转换程序,得到以下程序:
#define SIZE (1024)
int main(int argc, char* argv[]) {
int sum = 0;
int* array = malloc(sizeof(int) * SIZE);
int sum0 = 0;
int sum1 = 0;
int sum2 = 0;
int sum3 = 0;
/* 4x4 unrolling */
unsigned long long before = __rdtsc();
for (int i = 0; i < SIZE; i += 4) {
sum0 += array[i];
sum1 += array[i + 1];
sum2 += array[i + 2];
sum3 += array[i + 3];
}
unsigned long long after = __rdtsc();
sum = sum0 + sum1 + sum2 + sum3;
double cpe = (double)(after - before) / SIZE;
printf("CPE is %f\n", cpe);
printf("sum is %d\n", sum);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
请注意,我省略了处理 不是 4 的倍数情况的代码。SIZE该程序报告 CPE 约为 0.80。
我的程序运行在AMD 5950X上,根据AMD的软件优化手册(https://developer.amd.com/resources/developer-guides-manuals/),整数加法指令的延迟为1个周期,吞吐量为4每个周期的指令。它还具有一个加载存储单元,可以同时执行三个独立的加载操作。我对CPE的期望是0.33,我不知道为什么结果高这么多。
我的编译器是 gcc 12.2.0。所有程序均使用标志进行编译-Og。
我检查了优化程序的汇编代码,但没有发现任何帮助:
.L4:
movslq %r9d, %rcx
addl (%r8,%rcx,4), %r11d
addl 4(%r8,%rcx,4), %r10d
addl 8(%r8,%rcx,4), %ebx
addl 12(%r8,%rcx,4), %esi
addl $4, %r9d
.L3:
cmpl $127, %r9d
jle .L4
Run Code Online (Sandbox Code Playgroud)
我假设 4 条指令中至少有 3 条addl应该并行执行。然而,程序的结果并没有达到我的预期。
cmpl $127, %r9drdtsc与退出循环时的开销和分支错误预测以及 CPU 升至最大频率的时间相比,迭代计数并不大。
此外,您想要测量核心时钟周期,而不是 TSC 参考周期。将循环放入静态可执行文件中(以最小化启动开销)并运行它perf stat以获取整个过程的核心时钟。(如x86的MOV真的可以“免费”吗?为什么我根本不能重现这个?或者perf我在其他答案中发布的一些实验。)
10M 到 1000M 的总迭代次数是合适的,因为这还不到一秒,而且我们只想测量稳态行为,而不是冷缓存或冷分支预测器效应。或者页面错误。空闲系统上的中断开销往往低于 1%。用于perf stat --all-user仅计算用户空间周期和指令。
如果您想对数组执行此操作(而不是仅从 asm 中删除指针增量),请对一个小型(16K)数组进行多次传递,以便它们全部命中 L1d 缓存。使用嵌套循环,或使用 anand来包装索引。
这样做,是的,您应该能够测量add mem, regZen3 及更高版本的 3/时钟吞吐量,即使您留下movslq编译器输出中的开销和垃圾-Og。
当您真正进行微基准测试以找出有关一种指令形式的吞吐量的信息时,手动编写汇编通常比哄骗编译器发出您想要的循环更容易。(只要您了解足够的汇编以避免陷阱,例如.balign 64在循环之前只是为了更好地采取措施,以期避免前端瓶颈。)
另请参阅https://uops.info/了解它们的测量方式;对于任何给定的测试,您可以单击链接查看他们运行的实验的 asm 循环体,以及测试中每个变体的原始性能计数器输出。(虽然我不得不承认我忘记了 MPERF 和 APERF 对于 AMD CPU 意味着什么;Intel CPU 的结果更明显。)例如https://uops.info/html-tp/ZEN3/ADD_R32_M32-Measurements.html是 Zen3结果,其中包括add reg, [r14+const]作为内循环体的 4 或 8 个独立指令的测试。
他们还测试了索引寻址模式。在“unroll_count = 200并且没有内部循环”的情况下,他们在索引寻址模式与非索引寻址模式下对 4 个独立添加的 MPERF / APERF / UOPS 获得了相同的结果。(它们的循环没有指针增量。)