尽管迭代之间存在依赖关系,循环仍需要不到 1 个周期

Loop takes less than 1 cycle despite dependency between iterations

我想对在我的 Skylake (i5-6500) 上进行一次添加所需的时间进行基准测试 CPU。 C对我来说已经足够低级了,所以我写了下面的代码:

// Initializing stuffs
int a = rand();
int b = rand();
const unsigned long loop_count = 1000000000;
unsigned int ignored; // used for __rdtscp 

// Warming up whatever needs to be warmed up
for (int i = 0; i < 100000; i++) {
    asm volatile("" : "+r" (a)); // prevents Clang from replacing the loop with a multiplication
    a += b;
}

// The actual measurement
uint64_t timer = __rdtscp(&ignored);
for (unsigned long i = 0; i < loop_count; i++) {
    asm volatile("" : "+r" (a)); // prevents Clang from replacing the loop with a multiplication
    a += b;
}
timer = __rdtscp(&ignored) - timer;

printf("%.2f cycles/iteration\n", (double)timer / loop_count);

使用 Clang 7.0.0 -O3 编译,我得到以下程序集(仅用于循环):

# %bb.2:
    rdtscp
    movq    %rdx, %rdi
    movl    %ecx, 4(%rsp)
    shlq    , %rdi
    orq %rax, %rdi
    movl    00000000, %eax       # imm = 0x3B9ACA00
    .p2align    4, 0x90
.LBB0_3:                                # =>This Inner Loop Header: Depth=1
    #APP
    #NO_APP
    addl    %esi, %ebx
    addq    $-1, %rax
    jne .LBB0_3
# %bb.4:
    rdtscp

和运行这段代码输出

0.94 cycles/iteration

(或几乎总是介于 0.93 和 0.96 之间的数字)

我很惊讶这个循环可以在不到 1 cycle/iteration 的时间内执行,因为对 a 的数据依赖性应该阻止 a += b 的并行执行。

IACA 还确认预期吞吐量为 0.96 个周期。另一方面,llvm-mca 预测总共需要 104 个周期来执行循环的 100 次迭代。 (如果需要,我可以在痕迹中进行编辑;让我知道)

当我使用 SSE 寄存器而不是通用寄存器时,我观察到类似的行为。

我可以想象 CPU 足够聪明,可以注意到 b 是常数,并且由于加法是可交换的,它可以展开循环并以某种方式优化加法。但是,我从来没有听说过也没有读过任何关于此的内容。此外,如果这是正在发生的事情,我希望性能比 0.94 cycles/iteration.

更好(ie. 少于 cycles/iteration)

这是怎么回事?这个循环如何能够在每次迭代不到 1 个周期内执行?


一些背景,为了完整性。如果您对我为什么尝试对单个添加进行基准测试不感兴趣,请忽略问题的其余部分。

我知道有一些工具(例如 llvm-exegesis)旨在对单个指令进行基准测试,我应该代替它们(或者只是看看 agner fog 的文档)。但是,我实际上是在尝试 compare three different additions:一个在循环中进行一次加法(我的问题的对象);一个在每个循环中进行 3 次加法(在 SSE 寄存器上,这应该最大限度地使用端口并且不受数据依赖性的限制),以及一个在软件中将加法实现为电路的方法。虽然结果基本符合我的预期; 0.94 cycles/iteration 对于在循环中添加一次的版本让我感到困惑。

核心频率和TSC频率可以不同。您的循环预计在每次迭代 1 核心周期 时 运行。如果在循环执行期间核心频率恰好是 TSC 频率的两倍,则每次迭代的吞吐量将为 0.5 TSC 周期,相当于 1 每次迭代的核心周期

在您的情况下,平均核心频率似乎略高于 TSC 频率。如果你不想在做实验时考虑动态频率缩放,那么将核心频率固定为等于 TSC 频率会更容易,这样你就不必转换数字。否则,您还必须测量核心频率的平均值。

在支持按内核频率缩放的处理器上,您必须固定所有内核的频率或将实验固定到具有固定频率的单个内核。或者,您可以使用 perf 之类的工具,以核心周期或秒为单位轻松测量时间,而不是以 TSC 周期进行测量。

另请参阅:How to get the CPU cycle count in x86_64 from C++?