为什么(a * b!= 0)在Java中比(a!= 0 && b!= 0)快?
我正在用Java编写一些代码,在某些情况下,程序stream由两个intvariables“a”和“b”是否非零来确定(注意:a和b从不是负数,从不在整数溢出范围内)。
我可以用它来评估它
if (a != 0 && b != 0) { /* Some code */ }
或者,
if (a*b != 0) { /* Some code */ }
因为我希望那段代码每次运行数百万次,所以我想知道哪一个会更快。 我通过在一个巨大的随机生成的数组上进行比较来做实验,我也很好奇数组的稀疏性(数据分数= 0)如何影响结果:
long time; final int len = 50000000; int arbitrary = 0; int[][] nums = new int[2][len]; for (double fraction = 0 ; fraction <= 0.9 ; fraction += 0.0078125) { for(int i = 0 ; i < 2 ; i++) { for(int j = 0 ; j < len ; j++) { double random = Math.random(); if(random < fraction) nums[i][j] = 0; else nums[i][j] = (int) (random*15 + 1); } } time = System.currentTimeMillis(); for(int i = 0 ; i < len ; i++) { if( /*insert nums[0][i]*nums[1][i]!=0 or nums[0][i]!=0 && nums[1][i]!=0*/ ) arbitrary++; } System.out.println(System.currentTimeMillis() - time); }
结果表明,如果你期望“a”或“b”等于0的时间超过〜3%, a*b != 0
比a!=0 && b!=0
更快:
我很想知道为什么。 任何人都可以点亮一下吗? 它是编译器还是硬件级?
编辑: 出于好奇…现在,我了解了分支预测,我想知道什么模拟比较会显示一个OR b是非零:
我们的确看到了与预期相同的分支预测效果,有趣的是该图沿着X轴有些翻转。
更新
我添加了!(a==0 || b==0)
来分析,看看会发生什么。
我还包括a != 0 || b != 0
a != 0 || b != 0
, (a+b) != 0
, (a|b) != 0
,出于好奇,学习了分支预测。 但是它们在逻辑上不等于其他expression式,因为只有一个OR b需要非零才能返回true,所以它们并不意味着要比较处理效率。
我还添加了我用于分析的实际基准,它只是迭代一个任意的intvariables。
4-有些人build议包括a != 0 & b != 0
,而不是a != 0 && b != 0
,预测它会更接近a*b != 0
因为我们会删除分支预测效果。 我不知道&
可以用于布尔variables,我以为它只用于整数的二进制操作。
注意:在我正在考虑所有这些的情况下,int溢出不是一个问题,但这在一般情况下绝对是一个重要的考虑因素。
CPU:Intel Core i7-3610QM @ 2.3GHz
Java版本:1.8.0_45
Java(TM)SE运行时环境(build 1.8.0_45-b14)
Java HotSpot(TM)64位服务器虚拟机(构build25.45-b02,混合模式)
我忽略了你的基准可能存在缺陷的问题,并以结果为准。
它是编译器还是硬件级?
后者,我认为:
if (a != 0 && b != 0)
将编译为2个内存负载和两个条件分支
if (a * b != 0)
将编译为2个内存加载,一个乘法和一个条件分支。
如果硬件级分支预测无效,则乘法可能比第二条件分支更快。 当你增加比率…分支预测变得不那么有效。
条件分支慢的原因是它们导致指令执行pipe道停顿。 分支预测是通过预测分支将要去哪个方向并基于此来推测性地select下一条指令来避免失速。 如果预测失败,则在加载另一个方向的指令时有延迟。
(注意:上面的解释过于简单,为了更准确的解释,您需要查看CPU制造商提供的关于汇编语言编码器和编译器编写者的文献, 分支预测器上的Wikipedia页面是很好的背景。
但是,有一点你需要小心这个优化。 有没有什么值的地方a * b != 0
会给出错误的答案? 考虑计算产品导致整数溢出的情况。
UPDATE
你的图表倾向于证实我说的话。
-
在条件分支
a * b != 0
情况下,还有一个“分支预测”效果,这在图中出现。 -
如果将X轴上的曲线投影到0.9以上,则看起来像1)它们将在约1.0和2处相遇)交会点将与X = 0.0大致相同的Y值。
更新2
我不明白为什么曲线是不同的a + b != 0
和a | b != 0
a | b != 0
例。 分支预测器逻辑中可能有一些聪明的东西。 或者它可能表明别的东西。
(请注意,这种事情可能是特定的芯片型号或甚至版本,您的基准testing结果可能会在其他系统上有所不同。
但是,它们都具有为a
和b
所有非负值工作的优点。
我认为你的基准有一些缺陷,可能对推断真正的节目没有用处。 这是我的想法:
-
(a*b)!=0
对于溢出的值会做错误的事情,而(a+b)!=0
会额外地对正数和负数的数值进行和为零的错误操作,所以你不能使用在一般情况下,即使他们在这里工作的expression。 -
(a|b)!=0
和(a+b)!=0
正在testing是否两个值都不为零,而(a*b)!=0
和a != 0 && b != 0
是非零的。 这两种情况在相同百分比的数据上是不一样的。 -
VM在外部(
fraction
)循环的头几次运行期间将优化expression式,当fraction
为0时,几乎不会采用分支。 如果你以0.5开始fraction
,优化器可能做不同的事情。 -
除非虚拟机能够消除这里的一些数组边界检查,否则在expression式中还有其他四个分支只是由于边界检查,这是一个复杂的因素,试图找出发生在低级别的事情。 如果将二维数组拆分为两个平面数组,将
nums[0][i]
和nums[1][i]
改为nums0[i]
和nums1[i]
可能会得到不同的结果。 -
CPU分支预测器尝试检测数据中的短格式,或者运行所有分支采取或不采取。 随机生成的基准数据是分支预测器尝试处理的最糟糕的事情。 如果您的真实数据具有可预测的模式,或者长时间运行全零和全零非零值,则分支可能会花费较less。
-
满足条件后执行的特定代码可能会影响评估条件本身的性能,因为它会影响事件,如是否可以展开循环,哪些CPU寄存器可用,以及是否有任何获取的
nums
值需要在评估条件后重新使用。 仅仅增加基准中的计数器对于真正的代码来说不是一个完美的占位符。 -
System.currentTimeMillis()
在大多数系统上不会比+/- 10 ms更准确。System.nanoTime()
通常更准确。
正如你所看到的那样,有很多不确定因素,通过这种微观优化总是很难说明确的,因为在一个虚拟机或CPU上更快的技巧可能会更慢。 如果您的虚拟机是HotSpot,请注意有两种更多的变种,与“服务器”虚拟机相比,“客户机”虚拟机具有不同(较弱)的优化。
如果你可以反汇编虚拟机生成的机器码 ,那么不要试图猜测它做了什么!
这里的答案是好的,但我有一个想法可以改善事情。
由于两个分支和相关的分支预测是可能的罪魁祸首,所以我们可以在不改变逻辑的情况下将分支减less到单个分支。
bool aNotZero = (nums[0][i] != 0); bool bNotZero = (nums[1][i] != 0); if (aNotZero && bNotZero) { /* Some code */ }
它也可能有用
int a = nums[0][i]; int b = nums[1][i]; if (a != 0 && b != 0) { /* Some code */ }
原因是,根据短路规则,如果第一个布尔值为假,则不应评估第二个布尔值。 如果nums[0][i]
为假,它必须执行额外的分支来避免评估nums[1][i]
。 现在,你可能不关心nums[1][i]
被评估了,但是编译器不能确定当你这样做的时候它不会抛出一个超出范围或null的参考。 通过将if块减less到简单的布尔值,编译器可能足够聪明地认识到不必要地评估第二个布尔值不会有负面影响。
当我们乘法时,即使一个数字是0,那么产品也是0
(a*b != 0)
它评估产品的结果,从而消除从0开始的迭代的前几次发生。结果比较的结果小于当条件是
(a != 0 && b != 0)
每个元素与0进行比较并进行评估。 因此所需时间较less。 但我相信第二个条件可能会给你更准确的解决scheme。
您正在使用随机input数据,使分支不可预知。 在实践中,分支往往(〜90%)可预测,所以在实际代码中分支代码可能会更快。
那就说。 我看不出a*b != 0
比(a|b) != 0
更快。 通常,整数乘法比按位“或”更昂贵。 但这样的事情偶尔会变得怪异。 请参阅“ 处理器caching效果 ” 库中的“示例7:硬件复杂性”示例。