【问题标题】:conditional data transfers VS n conditional control transfers(using conditional mov) in Assembly条件数据传输 VS n 条件控制传输(使用条件 mov)在 Assembly
【发布时间】:2019-02-03 22:17:30
【问题描述】:

您好,我正在阅读一本比较条件数据传输的教科书 和装配中的条件控制转移:

上面是gotodiff(条件跳转)


下面是 cmovdiff(条件 mov)

不知道为什么

v = test-expr ? then-expr : else-expr;

比以下更有效:

if (!test-expr)
goto false;
v = true-expr;
goto done;
false:
v = else-expr;
done:

假设分支预测硬件会正确猜测 大约 50% 的时间,所以如果我们每次运行两次(第一次预测成功,第二次预测不成功,gotodiff 总共有 6+8 = 14 条指令要执行,而 cmovdiff 将有 8+8 = 16条指令,为什么cmovdiff比gotodiff效率更高?

【问题讨论】:

  • 您在 C 级别上的问题没有多大意义,因为任何打开优化的体面 C 编译器都将以相同的方式进行翻译(条件 mov 或分支,取决于编译目标) .在汇编级别的问题是,指令数在现代 CPU 上并不那么重要,它们通常可以在“单个”时钟中执行多达 2 条以上的指令,但这需要在前面的步骤中完成大量工作作为准备。分支是非常昂贵的操作,因为准备工作要么缺少一个选项,要么必须推测两个分支。
  • 虽然条件 mov 仅使结果难以预测,因此任何依赖于 eax 的进一步指令都必须等到条件最终确定并且结果已知,但即使是那些也可能已经 fetch+decoded+prepared,并且其他不依赖于这些指令的指令甚至可以完全执行(乱序),因此条件 mov 对并行乱序复杂事物的压力较小,即现代 x86 CPU。 (推测整个分支比在单个队列中停止依赖指令要困难得多)。

标签: assembly cpu instruction-set


【解决方案1】:

您大大低估了分行失误的成本。检测到分支未命中后需要多个周期才能恢复(分支提取/解码后的许多流水线阶段)。 What exactly happens when a skylake CPU mispredicts a branch?.

并且由于某种原因,您假设错误推测不会继续到以后的说明中(此处未显示)。


另外,将指令计数相加远不能准确估算吞吐量或延迟成本。有些指令有或多或少的延迟;例如,许多最近的 x86 CPU 具有零延迟mov。 (Can x86's MOV really be "free"? Why can't I reproduce this at all?)。不过,就吞吐量而言,mov 仍然需要消耗前端带宽。

请参阅What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?,了解如何实际上进行静态分析,以确定某些代码在现代 x86 上运行的速度(在正确预测的情况下)。剧透:它很复杂,有时对吞吐量最佳的东西对延迟不是最佳的,因此选择取决于周围的代码。 (对独立数据进行处理,而不是一个长链,其中一个的输出是下一个的输入。)

cmov 在 Broadwell 之前在 Intel 上是 2 微指令,并且控制依赖关系脱离了关键路径(由于推测执行)。因此,通过正确的预测,分支代码可能会比在计算两者后具有数据依赖性来选择正确的结果更好(http://yarchive.net/comp/linux/cmov.html)。这是一个cmov 是悲观的案例:gcc optimization flag -O3 makes code slower than -O2


如果分支预测在 50% 的情况下失败(即不比机会好!),包括 cmov 版本的循环可能会快 10 倍。

@Mysticial 的分支与无分支的实际基准测试显示,在这种情况下,无分支在随机(未排序)数组上的加速因子为 5:

       if (data[c] >= 128)
            sum += data[c];

Why is it faster to process a sorted array than an unsorted array?。 (对于已排序的数据,在这种情况下,branchy 只会稍微快一些。显然,其他一些瓶颈阻止了 branchy 在 Mysticial 的 Nehalem 上的速度更快。)


50% 的分支错误预测率绝对是可怕的,几乎是最坏的情况。即使是 20% 也很糟糕;如果分支历史中有任何类型的模式,现代分支预测器非常好。例如如果每次都使用相同的输入数据作为微基准测试的一部分重复执行,Skylake 可以完全预测 BubbleSort 中超过 12 个元素的分支。

但是在随机数据上循环可能是高度不可预测的。


您的源代码全是图像,因此我无法将其复制/粘贴到 Godbolt 编译器资源管理器 (https://godbolt.org/) 并查看使用 -O3 编译时现代 gcc 和 clang 的作用。我怀疑它会比您显示的代码示例更有效:看起来很多movcmov 应该能够使用sub 设置的标志。这是我对您的absdiff 函数的期望(或至少希望):

# loading args into registers not shown, only necessary with crappy stack-args calling conventions

# branchless absdiff.  Hand-written.
# Compilers should do something like this if they choose branchless at all.

# inputs in x=%edi, y=%esi
mov   %edi, %eax
sub   %esi, %eax     # EAX= x-y
sub   %edi, %esi     # ESI= y-x
cmovg %esi, %eax     # if(y>x) eax=y-x

# result in EAX

在 Broadwell/Skylake 或锐龙:

  • 两条sub 指令可以在同一个周期上运行(因为mov 是零延迟,假设mov-elimination 成功)。
  • cmovg 在此之后的循环中准备好其输入,并在另一个循环中产生结果。

所以我们有 4 个 uops,从 x 和 y 都准备好了 2 个周期延迟。 Haswell 和更早的版本是具有额外延迟周期的额外微指令,因为cmov 有 3 个输入,而早期的英特尔 CPU 无法将 3 输入指令解码为单个微指令。


您显示的编译器输出很差;太多mov 并使用单独的cmpcmp 只是一个 sub,它只写入标志,而不是整数寄存器操作数,我们已经需要 sub 结果。

如果您有一个编译器在启用完全优化的情况下发出类似的东西,而不是我上面显示的,请报告一个错过优化的错误。


正如@Ped7g 所说,编译器可以对if() 使用无分支,或者如果他们决定使用三元分支。如果您正在处理数组元素而不是局部变量,那么三元往往会有所帮助,因为无条件地编写变量可以让编译器进行优化,而不必担心踩到另一个线程正在执行的操作。 (即编译器不能发明对潜在共享变量的写入,但三元运算符总是写入。)

how to force the use of cmov in gcc and VS

AVX-512 and Branching

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-13
    • 1970-01-01
    相关资源
    最近更新 更多