【问题标题】:How can I make branchless code?如何制作无分支代码?
【发布时间】:2015-11-13 10:29:17
【问题描述】:

与此答案相关:https://stackoverflow.com/a/11227902/4714970

在上面的答案中,提到了如何通过避免分支来避免分支预测失败。

用户通过替换来证明这一点:

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

与:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

这两者如何等价(针对具体数据集,并非严格等价)?

在类似的情况下,我可以通过哪些一般方法来做类似的事情?是否总是使用>>~

【问题讨论】:

  • hackersdelight.org 是一个很好的短函数集合,经常利用聪明的位黑客。我认为还有另一个系列更侧重于你所说的比特黑客,但我想不起来。
  • 一些编译器可以用无分支的cmov 指令替换条件运算符?:
  • hackersdelight.org 似乎已经死了。悲伤的时光。

标签: java performance branch-prediction


【解决方案1】:
int t = (data[c] - 128) >> 31;

这里的技巧是,如果data[c] >= 128,那么data[c] - 128 是非负的,否则是负的。 int 中的最高位,即符号位,当且仅当该数字为负时为 1。 >> 是扩展符号位的移位,因此向右移位 31 会使 whole 结果为 0,如果它曾经是非负数,则所有 1 位(表示 -1)如果它曾经是是消极的。所以如果data[c] >= 128t0,否则-1~t 切换这些可能性,所以如果data[c] >= 128~t-1,否则0

x & (-1) 始终等于 xx & 0 始终等于 0。因此,如果data[c] < 128sum += ~t & data[c] 增加sum 0,否则增加data[c]

其中许多技巧可以应用到其他地方。当且仅当一个值大于或等于另一个值时,这个技巧当然可以普遍应用于有一个数字为0,否则为-1,你可以再弄乱它以获得<=<,等等。像这样的位旋转是使数学运算无分支的常用方法,尽管它肯定不会总是由相同的运算构建而成; ^ (xor) 和 | (or) 有时也会发挥作用。

【讨论】:

  • if data[c] >= 128, then ... - 仅当减法没有带符号环绕时。 例如INT_MIN - 128 换行成为一个正整数。这就是为什么 asm 中的有符号条件检查溢出和符号标志,而不仅仅是符号。例如在 x86 上,cmovge(大于或等于)检查 SF == OF。如果data[c] 是一种更窄的类型,可以符号扩展为int,则仅查看符号位是安全的,因为这使得符号溢出不可能。或者在这种情况下,如果您知道 data[c] >= INT_MIN+128 将始终为真。
【解决方案2】:

虽然 Louis Wasserman 的回答是正确的,但我想向您展示一种更通用(也更清晰)的方式来编写无分支代码。您可以只使用? : 运算符:

    int t = data[c];
    sum += (t >= 128 ? t : 0);

JIT 编译器从执行配置文件中看到,这里的条件预测不佳。在这种情况下,编译器足够聪明,可以用条件移动指令替换条件分支:

    mov    0x10(%r14,%rbp,4),%r9d  ; load R9d from array
    cmp    $0x80,%r9d              ; compare with 128
    cmovl  %r8d,%r9d               ; if less, move R8d (which is 0) to R9d

您可以验证自己,此版本对已排序和未排序数组的运行速度同样快。

【讨论】:

  • 请注意,当分支可预测时,将控制依赖关系转变为关键路径上的数据依赖关系可能是一件坏事。 cmov 将其两个输入都放入依赖链中。此外,好的编译器通常可以将简单的if 子句转换为cmov
  • 是的,这样做可以提高已排序和未排序的速度,但如果未排序,则不如使其无分支有效。查看结果 -> 未排序 = 11.3 秒,?: op = 9.18 秒,无分支排序 = 5.89 秒,无分支未排序 = 5.86,排序 = 5.56 秒和 ?:op 排序 = 4.77 秒
  • 这种方法的问题在于您依赖于编译器优化。因此,如果您想确保可以无分支代码(例如,为了避免密码学代码中的计时攻击),使用丑陋的解决方法仍然是一个好主意(即使它也不是 100% 保证)。
  • ? : 是一个分支,与if() 相比,它的优势在于它是一个表达式而不是一个语句。具有if() 和两个returns 的内联函数基本上等同于? :。如果它导致无分支机器语言,那是由于优化。请参阅@aki-suihkonen 的回答stackoverflow.com/a/32113018/260122 进行更长时间的讨论。
  • @clacke 我不是说它不是一个分支。我也不说是。它是一种运算符,一种语言结构。它可能会或可能不会被编译到一个分支。在 Java 字节码中有一个分支(只是因为除了分支之外没有其他条件字节码)。但我要说的是,JIT 编译器会识别这种特定的字节码模式并将其转换为条件移动指令。
【解决方案3】:

无分支代码意味着通常使用集合 [0, 1] 中的 权重 评估条件语句的所有可能结果,因此 Sum{ weight_i } = 1。大多数计算本质上是丢弃。一些优化可能源于这样一个事实,即当相应的权重 w_i(或掩码 m_i)为零时,E_i 不一定是正确的。

  result = (w_0 * E_0) + (w_1 * E_1) + ... + (w_n * E_n)    ;; or
  result = (m_0 & E_0) | (m_1 & E_1) | ... | (m_n * E_n)

其中 m_i 代表位掩码。

速度也可以通过水平折叠的 E_i 并行处理来实现。

这与 if (a) b; else c; 的语义相矛盾,或者它是三元简写 a ? b : c,其中仅计算 [b, c] 中的一个表达式。

因此,三元运算对于无分支代码来说并不是灵丹妙药。一个体面的编译器同样可以生成无分支代码

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

对比

    movl    -4(%rdi,%rdx), %ecx
    leal    (%rax,%rcx), %esi
    addl    $-128, %ecx
    cmovge  %esi, %eax

无分支代码的变体包括通过其他无分支非线性函数(例如 ABS,如果存在于目标机器中)来呈现问题。

例如

 2 * min(a,b) = a + b - ABS(a - b),
 2 * max(a,b) = a + b + ABS(a - b)

甚至:

 ABS(x) = sqrt(x*x)      ;; caveat -- this is "probably" not efficient

除了<<~,使用bool!bool 代替(可能未定义)(int >> 31)可能同样有益。同样,如果条件评估为 [0, 1],则可以生成一个带有否定的工作掩码:

-[0, 1] = [0, 0xffffffff]  in 2's complement representation

【讨论】:

    猜你喜欢
    • 2018-04-24
    • 2021-09-25
    • 1970-01-01
    • 2013-09-17
    • 1970-01-01
    • 2019-12-31
    • 2018-08-27
    • 1970-01-01
    • 2017-03-11
    相关资源
    最近更新 更多