【问题标题】:Loop takes less than 1 cycle despite dependency between iterations尽管迭代之间存在依赖关系,但循环花费不到 1 个周期
【发布时间】:2020-05-04 10:32:44
【问题描述】:

我想对在我的 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    $32, %rdi
    orq %rax, %rdi
    movl    $1000000000, %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 个周期/迭代中执行,因为对 a 的数据依赖应该会阻止 a += b 的并行执行。

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

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

我可以想象 CPU 足够聪明,可以注意到b 是恒定的,并且由于加法是可交换的,它可以展开循环并以某种方式优化加法。然而,我从来没有听说过也没有读过任何关于这个的东西。此外,如果这是正在发生的事情,我预计性能会比 0.94 个周期/迭代更好(即。 更少的周期/迭代)。

发生了什么事?这个循环如何能够在每次迭代不到 1 个周期内执行?


为了完整起见,有些背景。如果您对我为什么要对单个添加进行基准测试不感兴趣,请忽略剩下的问题。

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

【问题讨论】:

  • 你芯片上的TSC频率是多少?运行dmesg | grep 'tsc' 找出答案。核心频率是多少?核心频率是固定的吗?
  • @HadiBrais 不错。修复CPU频率解决了这个问题。我认为将 cpufreq 调速器设置为“性能”就足够了。但是我已经将频率设置为 3.2GHz (cpupower frequency-set -f 3.2G),现在我得到了 1.00 个周期/迭代,正如预期的那样。想把它作为答案发布吗? ;)
  • 可以作为How to get the CPU cycle count in x86_64 from C++? 的副本关闭,我的回答解释了 RDTSC 在参考周期中计数。可能有一个更具体的问题,这个问题更像你的问题,答案就集中在这个问题上。这是关于 RDTSC 工作原理的一个众所周知的事实,所以它似乎不值得一个 long 答案,但如果有人想写一个带有链接的简短答案。跨度>

标签: performance x86 intel rdtsc


【解决方案1】:

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

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

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

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

【讨论】:

  • Re: 首先使用perf 测量核心时钟周期:我经常在一个程序上使用perf stat,该程序的工作负载处于一个大的重复循环中,因此它完全控制了启动开销当运行 100 毫秒到 1 秒时,如果您想要更重要的准确度数字,则运行时间更长。但是,如果您的循环瓶颈以某种方式导致每次迭代的整个周期数,那么您真的不需要打扰。 (理想情况下是静态链接,在 asm 中编写时很容易,或者在 C 中编写 _start 并以 asm 退出。)或者更高级的选项是进行设置,以便您可以从用户空间 rdpmc
  • @PeterCordes 为什么你不需要在这种情况下打扰?因为有效载荷中的循环数是整数,所以数字总是像a,aaa,000,xxx一样出现,你可以看看a,aaa部分?
  • @BeeOnRope:是的,没错。如果您可以假设周期/迭代的非整数部分只是噪声,那么即使是 10 毫秒的定时运行也可以。 (如果您不做需要在第一次通过页面错误的许多页面的内存工作......在这种情况下,首先假设整数瓶颈将是一个糟糕的假设。内存访问使一切变得复杂。)跨度>
猜你喜欢
  • 1970-01-01
  • 2021-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多