为什么(a * b!= 0)在Java中比(a!= 0 && b!= 0)更快?

我在Java中编写了一些代码,在某些时候,程序的流程由两个int变量“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 != 0a!=0 && b!=0更快:

AND b非零结果的图形图表

我很想知道为什么。 任何人都可以点亮一下吗? 它是编译器还是硬件级别的?

编辑:出于好奇...现在我了解了分支预测,我想知道模拟比较对于OR b显示的是非零的:

a或b的图非零

我们的确看到了与预期相同的分支预测效果,有趣的是该图沿着X轴有些翻转。

更新

我添加了!(a==0 || b==0)来分析,看看会发生什么。

我还包括a != 0 || b != 0 在了解分支预测之后,出于好奇, a != 0 || b != 0(a+b) != 0(a|b) != 0 。 但是它们在逻辑上不等同于其他表达式,因为只有一个OR b需要非零才能返回true,所以它们不能用于处理效率的比较。

我还添加了我用于分析的实际基准,它只是迭代一个任意的int变量。

4-有些人建议包括a != 0 & b != 0而不是a != 0 && b != 0 ,预测它会更接近a*b != 0因为我们会删除分支预测效果。 我不知道&可以与布尔变量一起使用,我认为它仅用于具有整数的二元运算。

注意:在我正在考虑所有这些的情况下,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位服务器虚拟机(构建25.45-b02,混合模式)


我忽略了您的基准测试可能存在缺陷的问题,并以结果为准。

它是编译器还是硬件级别的?

后者,我认为:

  if (a != 0 && b != 0)

将编译为2个内存负载和两个条件分支

  if (a * b != 0)

将编译为2个内存加载,一个乘法和一个条件分支。

如果硬件级分支预测无效,则乘法可能比第二个条件分支更快。 随着您提高比率...分支预测变得不那么有效。

条件分支较慢的原因是它们导致指令执行管道停顿。 分支预测是通过预测分支将要去哪个方向并基于此推测选择下一条指令来避免失速。 如果预测失败,则加载另一个方向的指令时出现延迟。

(注意:上面的解释过于简单,为了更准确的解释,您需要查看CPU制造商为汇编语言编码器和编译器编写者提供的文献,分支预测器上的维基百科页面是良好的背景。)


但是,有一件事你需要注意这个优化。 有a * b != 0会给出错误答案的任何值吗? 考虑计算产品导致整数溢出的情况。


UPDATE

你的图表倾向于证实我说的话。

  • 在条件分支a * b != 0情况下,还存在“分支预测”效果,并且这在图中出现。

  • 如果将X轴上的曲线投影到0.9以上,看起来像1)它们将在大约1.0和2处相遇),会合点将大致与X = 0.0时的Y值相同。


  • 更新2

    我不明白为什么a + b != 0a | b != 0的曲线不同 a | b != 0例。 分支预测器逻辑中可能有一些聪明的东西。 或者它可能表明别的东西。

    (请注意,这种事情可能是特定的芯片型号或甚至是版本,您的基准测试结果可能与其他系统不同。)

    但是,它们都具有为ab所有非负值工作的优点。


    我认为你的基准有一些缺陷,可能对推断真正的节目没有用处。 这是我的想法:

  • (a*b)!=0对于溢出的值会做错误的事情,并且(a+b)!=0会额外地对总和为零的正值和负值做错误的事情,所以您不能使用在一般情况下,即使他们在这里工作的表达式。

  • (a|b)!=0(a+b)!=0正在测试,如果任一值是非零的,而(a*b)!=0a != 0 && b != 0正在测试如果两个非零。 这两种情况在相同百分比的数据上不会成立。

  • 虚拟机将在外部( fraction )循环的前几次运行期间优化表达式,当fraction为0时,几乎不会采用分支。 如果您以0.5开始fraction ,优化程序可能会做不同的事情。

  • 除非虚拟机能够消除这里的一些数组边界检查,否则表达式中还有其他四个分支只是由于边界检查,这是一个复杂的因素,当试图找出低层发生的事情时。 如果将二维数组拆分为两个平面数组,将nums[0][i]nums[1][i]改为nums0[i]nums1[i]可能会得到不同的结果。

  • CPU分支预测器尝试检测数据中的短模式,或者运行所有分支采取或不采取。 随机生成的基准数据是分支预测器尝试处理的最糟糕的事情。 如果您的真实数据具有可预测的模式,或者长时间运行全零和全非零值,则分支可能会花费很多。

  • 满足条件后执行的特定代码可能会影响评估条件本身的性能,因为它会影响事件,如是否可以展开循环,哪些CPU寄存器可用,以及是否有任何获取的nums值需要在评估条件后重新使用。 仅仅增加基准中的计数器对于真实代码的作用并不是一个完美的占位符。

  • System.currentTimeMillis()在大多数系统上不会比+/- 10毫秒更准确。 System.nanoTime()通常更准确。

  • 正如你所看到的那样存在很多不确定性,并且通过这种微型优化很难说明确任何事情,因为一个VM或CPU上更快的技巧在另一个VM上可能会变慢。 如果您的虚拟机是HotSpot,请注意有两种更多的变体,“客户机”虚拟机与“服务器”虚拟机相比具有不同(较弱)的优化。

    如果您可以反汇编虚拟机生成的机器码,那么不要试图猜测它做什么!


    这里的答案很好,但我有一个想法可以改善事情。

    由于两个分支和相关的分支预测是可能的罪魁祸首,我们可能能够在不改变逻辑的情况下将分支减少到单个分支。

    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块减少到简单的布尔值,编译器可以足够聪明地认识到,不必要地评估第二个布尔值不会产生负面影响。

    链接地址: http://www.djcxy.com/p/283.html

    上一篇: Why is (a*b != 0) faster than (a != 0 && b != 0) in Java?

    下一篇: Why is branch prediction faster than no branch at all?