assembly 为什么我的程序不能达到整数加法指令吞吐量的限制?

lyfkaqu1  于 2023-01-21  发布在  其他
关注(0)|答案(1)|浏览(117)

我已经阅读了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条应该并行执行。然而,程序的结果并不符合我的预期。

k2arahey

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次独立加法,得到了相同的结果。(他们的循环没有指针增量。)

相关问题