Java 如何生成无分支代码?
关于这一回答: 在上面的回答中,提到了如何通过避免分支来避免分支预测失败 用户通过替换以下内容来演示这一点:Java 如何生成无分支代码?,java,performance,branch-prediction,Java,Performance,Branch Prediction,关于这一回答: 在上面的回答中,提到了如何通过避免分支来避免分支预测失败 用户通过替换以下内容来演示这一点: if (data[c] >= 128) { sum += data[c]; } 与: 这两种数据是如何等价的(对于特定的数据集,不是严格等价的) 在相似的情况下,我可以用什么方法做相似的事情?是否总是使用>和~ int t = (data[c] - 128) >> 31; 这里的技巧是,如果data[c]>=128,则data[c]-128为非负,否则为负。
if (data[c] >= 128)
{
sum += data[c];
}
与:
这两种数据是如何等价的(对于特定的数据集,不是严格等价的)
在相似的情况下,我可以用什么方法做相似的事情?是否总是使用>
和~
int t = (data[c] - 128) >> 31;
这里的技巧是,如果data[c]>=128
,则data[c]-128
为非负,否则为负。int
中的最高位,即符号位,当且仅当该数字为负数时才为1>
是一个扩展符号位的移位,因此如果它过去是非负的,则向右移位31将使整个结果为0,如果它过去是负的,则所有1位(表示-1)。因此,如果数据[c]>=128,则t
为0
,否则为-1
~t
切换这些可能性,因此如果数据[c]>=128
,则~t
为-1
,否则为0
x&(-1)
始终等于x
,x&0
始终等于0
。因此sum+=~t&data[c]
如果data[c]<128
,则增加sum
,否则增加data[c]
其中许多技巧可以应用于其他地方。当且仅当一个值大于或等于另一个值时,此技巧当然可以普遍应用于将数字设为0
,否则设为-1
,并且您可以对其进行更多的处理以获得,而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
您可以自己验证这个版本对于排序数组和未排序数组都同样快。无分支代码意味着通常使用集合[0,1]中的权重来计算条件语句的所有可能结果,以便总和{weight_i}=1。大部分计算基本上都被丢弃了。当相应的权重w\u i
(或掩码m\u i
)为零时,E\u i
不必是正确的,这一事实可能会导致一些优化
其中m_i代表位掩码
速度也可以通过水平折叠并行处理E_i来实现
这与if(a)b的语义相矛盾;其他c代码>还是三元速记a?b:c
,其中只计算[b,c]中的一个表达式
因此,三值运算并不是无分支代码的灵丹妙药。一个像样的编译器为每个对象生成同样的无分支代码
t = data[n];
if (t >= 128) sum+=t;
vs
无分支代码的变体包括通过目标机器中存在的其他无分支非线性函数(如ABS)来表示问题
e、 g
甚至:
ABS(x) = sqrt(x*x) ;; caveat -- this is "probably" not efficient
除了
31)。同样,如果条件的计算结果为[0,1],则可以生成一个带否定的工作掩码:
-[0, 1] = [0, 0xffffffff] in 2's complement representation
是一个很好的短函数集合,通常利用巧妙的位攻击。我认为还有另一个集合更关注像你所说的比特黑客,但我想不出它是atm。一些编译器可以用无分支的cmov取代条件运算符?:
,
指令。hackersdelight.org似乎已经死了。不幸的时刻。幸运的是,它被存档了:请注意,当分支是可预测的时,将关键路径上的控制依赖项转换为数据依赖项可能是一件坏事cmov
将其两个输入都放入依赖链中。此外,好的编译器通常可以将简单的if
子句转换为cmov
。是的,这样做可以提高排序和未排序的速度,但如果未排序,效果不如使其无分支。请参见结果->未排序=11.3秒、:op=9.18秒、无分支排序=5.89秒、无分支未排序=5.86秒、排序=5.56秒和?:op排序=4.77秒。这种方法的问题是依赖于编译器优化。因此,如果您想确保可以使用无分支代码(例如,为了避免加密代码中的定时攻击),使用难看的解决方法仍然是一个好主意(即使它也不是100%的保证)。:
是一个分支,它只是比if()更具优势,它是一个表达式而不是一个语句。带有一个if()
和两个return
s的内联函数基本上等同于一个?:代码>。如果它导致无分支的机器语言,那是由于优化。请参阅@aki suikkonen的答案,了解更多讨论内容。@clacke我不认为这不是一个分支。我也不这么说。它是一个运算符,一种语言结构。它可以编译为分支,也可以不编译为分支。在Java字节码中有一个分支(只是因为除了分支之外没有其他条件字节码)。但我要说的是,JIT编译器识别这个特定的字节码模式,并将其转换为条件移动指令除非减法没有带符号的换行符。e、 g.INT_MIN-128
换行为正整数。这就是为什么asm中的签名条件会检查溢出和签名标志,而不仅仅是签名。e、 g.在x86上,(大于或等于)检查SF==OF
。如果data[c]
是将符号扩展到int
的较窄类型,则仅查看符号位是安全的,因为这会使签名覆盖
movl -4(%rdi,%rdx), %ecx
leal (%rax,%rcx), %esi
addl $-128, %ecx
cmovge %esi, %eax
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
-[0, 1] = [0, 0xffffffff] in 2's complement representation