如何制作无分支代码?
How can I make branchless code?
与此答案相关:
在上面的回答中,提到了如何通过避免分支来避免分支预测失败。
用户通过替换来演示:
if (data[c] >= 128)
{
sum += data[c];
}
有:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
这两个如何等价(针对具体数据集,不严格等价)?
在类似的情况下,我可以通过哪些通用方法来做类似的事情?是否总是使用 >>
和 ~
?
int t = (data[c] - 128) >> 31;
这里的技巧是,如果data[c] >= 128
,那么data[c] - 128
是非负的,否则就是负的。 int
中的最高位,即符号位,当且仅当该数字为负数时为 1。 >>
是扩展符号位的移位,因此右移 31 会使 whole 结果为 0,如果它以前是非负的,并且所有 1 位(表示 -1 ) 如果它曾经是负数。所以 t
如果 data[c] >= 128
是 0
,否则 -1
。 ~t
切换这些可能性,因此 ~t
如果 data[c] >= 128
是 -1
,否则 0
。
x & (-1)
始终等于 x
,而 x & 0
始终等于 0
。因此,如果 data[c] < 128
,sum += ~t & data[c]
将 sum
增加 0
,否则增加 data[c]
。
其中的许多技巧都可以应用到其他地方。当且仅当一个值大于或等于另一个值时,这个技巧当然可以普遍应用于使数字 0
,否则 -1
,你可以再弄乱它以获得<=
、<
等。像这样的位旋转是使数学运算无分支的常用方法,尽管它肯定不会总是由相同的运算构建; ^
(xor) 和 |
(or) 有时也会起作用。
虽然 Louis Wasserman 的回答是正确的,但我想向您展示一种更通用(也更清晰)的无分支代码编写方法。您可以只使用 ? :
运算符:
int t = data[c];
sum += (t >= 128 ? t : 0);
JIT 编译器从执行配置文件中看到这里的条件预测不佳。在这种情况下,编译器足够聪明,可以用条件移动指令替换条件分支:
mov 0x10(%r14,%rbp,4),%r9d ; load R9d from array
cmp [=11=]x80,%r9d ; compare with 128
cmovl %r8d,%r9d ; if less, move R8d (which is 0) to R9d
您可以自己验证此版本对排序数组和未排序数组的运行速度同样快。
无分支代码意味着通常评估条件语句的所有可能结果,其中 weight 来自集合 [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;
的语义矛盾,或者它是三元的 shorthand 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
与此答案相关:
在上面的回答中,提到了如何通过避免分支来避免分支预测失败。
用户通过替换来演示:
if (data[c] >= 128)
{
sum += data[c];
}
有:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
这两个如何等价(针对具体数据集,不严格等价)?
在类似的情况下,我可以通过哪些通用方法来做类似的事情?是否总是使用 >>
和 ~
?
int t = (data[c] - 128) >> 31;
这里的技巧是,如果data[c] >= 128
,那么data[c] - 128
是非负的,否则就是负的。 int
中的最高位,即符号位,当且仅当该数字为负数时为 1。 >>
是扩展符号位的移位,因此右移 31 会使 whole 结果为 0,如果它以前是非负的,并且所有 1 位(表示 -1 ) 如果它曾经是负数。所以 t
如果 data[c] >= 128
是 0
,否则 -1
。 ~t
切换这些可能性,因此 ~t
如果 data[c] >= 128
是 -1
,否则 0
。
x & (-1)
始终等于 x
,而 x & 0
始终等于 0
。因此,如果 data[c] < 128
,sum += ~t & data[c]
将 sum
增加 0
,否则增加 data[c]
。
其中的许多技巧都可以应用到其他地方。当且仅当一个值大于或等于另一个值时,这个技巧当然可以普遍应用于使数字 0
,否则 -1
,你可以再弄乱它以获得<=
、<
等。像这样的位旋转是使数学运算无分支的常用方法,尽管它肯定不会总是由相同的运算构建; ^
(xor) 和 |
(or) 有时也会起作用。
虽然 Louis Wasserman 的回答是正确的,但我想向您展示一种更通用(也更清晰)的无分支代码编写方法。您可以只使用 ? :
运算符:
int t = data[c];
sum += (t >= 128 ? t : 0);
JIT 编译器从执行配置文件中看到这里的条件预测不佳。在这种情况下,编译器足够聪明,可以用条件移动指令替换条件分支:
mov 0x10(%r14,%rbp,4),%r9d ; load R9d from array
cmp [=11=]x80,%r9d ; compare with 128
cmovl %r8d,%r9d ; if less, move R8d (which is 0) to R9d
您可以自己验证此版本对排序数组和未排序数组的运行速度同样快。
无分支代码意味着通常评估条件语句的所有可能结果,其中 weight 来自集合 [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;
的语义矛盾,或者它是三元的 shorthand 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