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

时间:2016-02-21 01:51:17

标签: java performance processing-efficiency microbenchmark branch-prediction

我正在用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”在大约3%的时间内等于0,a*b != 0a!=0 && b!=0更快:

Graphical graph of the results of a AND b non-zero

我很想知道原因。谁能解开一些光明?它是编译器还是硬件级别?

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

Graph of a or b non-zero

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

更新

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

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

3-我还添加了用于分析的实际基准,它只是迭代一个任意的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运行时环境(版本1.8.0_45-b14)
Java HotSpot(TM)64位服务器VM(版本25.45-b02,混合模式)

4 个答案:

答案 0 :(得分:229)

我忽略了您的基准可能存在缺陷的问题,并将结果视为面值。

  

是编译器还是硬件级别?

后者,我认为:

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

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

  if (a * b != 0)

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

如果硬件级分支预测无效,则乘法可能比第二个条件分支快。当你增加比率时......分支预测变得不那么有效了。

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

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

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

<强>更新

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

  • 在条件分支a * b != 0的情况下还有一个“分支预测”效果,这在图中显示出来。

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

更新2

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

(请注意,此类事物可能特定于特定芯片型号甚至版本。您的基准测试结果可能在其他系统上有所不同。)

但是,它们都具有处理ab的所有非负值的优势。

答案 1 :(得分:23)

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

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

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[1][i]为false,则必须执行额外的分支以避免评估nums[0][i]。现在,您可能不关心nums[1][i]是否被评估,但是编译器无法确定它是否会在您执行时抛出超出范围或null ref。通过将if块简化为简单的bool,编译器可能足够聪明,可以意识到不必要地评估第二个布尔值不会产生负面影响。

答案 2 :(得分:10)

当我们进行乘法运算时,即使一个数字为0,那么乘积为0.而写入

    (a*b != 0)

它评估产品的结果,从而消除从0开始的前几次迭代。结果,比较小于条件

时的比较
   (a != 0 && b != 0)

将每个元素与0进行比较并进行评估。因此,所需时间较少。但我相信第二个条件可能会给你更准确的解决方案。

答案 3 :(得分:8)

您正在使用随机输入数据,这使分支无法预测。在实践中,分支通常(约90%)是可预测的,因此在实际代码中,分支代码可能更快。

那就是说。我看不出a*b != 0如何比(a|b) != 0更快。通常,整数乘法比按位OR更昂​​贵。但像这样的事情偶尔会变得奇怪。例如,参见Gallery of Processor Cache Effects中的“示例7:硬件复杂性”示例。