我已经阅读了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;
}
它报告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;
}
注意,我省略了处理SIZE
不是4的倍数的情况的代码。此程序报告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
我假设4条addl
指令中至少有3条应该并行执行。然而,程序的结果并不符合我的预期。
1条答案
按热度按时间k2arahey1#
与
rdtsc
开销、退出循环时的分支预测失误以及CPU提升到最大频率所需的时间相比,cmpl $127, %r9d
的迭代次数并不多。此外,您还需要测量内核时钟周期,而不是TSC参考周期。将循环放入静态可执行文件中(为了最小的启动开销),并使用
perf stat
运行它,以获得整个过程的内核时钟。(如Can x86's MOV really be "free"? Why can't I reproduce this at all?或一些perf
实验,我已在其他答案中发布。)参见Idiomatic way of performance evaluation?
10 M到1000 M的总迭代次数是合适的,因为这仍然在一秒之内,我们只想测量稳态行为,而不是冷缓存或冷分支预测器的影响,或者页面错误。在空闲系统上,中断开销往往低于1%。使用
perf stat --all-user
只计算用户空间周期和指令。如果你想在一个数组上执行(而不是仅仅从asm中移除指针增量),那么就在一个小的(16 K)数组上执行多次遍历,这样它们都能在L1 d缓存中命中。
这样做,是的,你应该能够测量3/时钟吞吐量的
add mem, reg
在Zen 3和更高版本,即使你离开在movslq
开销和垃圾一样,从编译器-Og
输出。当您真正进行微基准测试以了解一种形式的指令的吞吐量时,手动编写asm通常比诱使编译器发出您想要的循环更容易(只要您知道足够的asm以避免陷阱,例如,在循环之前使用
.balign 64
只是为了更好地测量,以希望避免前端瓶颈)。有关它们的测量方式,请参见https://uops.info/;对于任何给定的测试,您都可以单击链接来查看他们运行的实验的asm循环体,以及测试中每个变量的原始perf计数器输出。(尽管我必须承认我忘记了MPERF和APERF对于AMD CPU的含义;英特尔CPU的结果更明显。)例如,https://uops.info/html-tp/ZEN3/ADD_R32_M32-Measurements.html是Zen 3结果,其包括对作为内部循环体的4或8个独立
add reg, [r14+const]
指令的测试。他们还测试了索引寻址模式。在“With unroll_count=200 and no inner loop”的情况下,他们在索引寻址模式和非索引寻址模式下,对MPERF / APERF / UOPS进行4次独立加法,得到了相同的结果。(他们的循环没有指针增量。)