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

我正在用Java写一些代码,在某些时候,程序的流是由两个int变量"a"和"b"非零(注意:a和b从不为负,也从不在整数溢出范围内)。

我可以用

if (a != 0 && b != 0) { /* Some code */ }

或者

if (a*b != 0) { /* Some code */ }

因为我希望这段代码每次运行数百万次,所以我想知道哪一个会更快。我通过在一个巨大的随机生成的数组中比较它们来做实验,我也很好奇数组的稀疏性(fraction of data = 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快:

 graphics graph of results of a AND b non-zero

我很想知道为什么。谁能给我一点启示?是编译器还是硬件层?

现在我了解了分支预测,我想知道模拟比较将显示什么 b是非零:

 a或b非零的图形

我们确实看到了与预期相同的分支预测效果,有趣的是,图在x轴上有些翻转。

更新

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

2-在学习了分支预测之后,出于好奇,我还加入了a != 0 || b != 0(a+b) != 0(a|b) != 0但它们在逻辑上并不等同于其他表达式,因为只有 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运行时环境(build 1.8.0_45-b14)
. exe Java HotSpot(TM) 64位服务器虚拟机(build 25.45-b02,混合模式)

24845 次浏览

我忽略了你的基准测试可能有缺陷的问题,并以表面价值的结果。

是编译器还是硬件层?

对于后者,我认为:

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

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

  if (a * b != 0)

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

如果硬件级分支预测无效,乘法可能比第二个条件分支快。随着比例的增加……分支预测变得不那么有效。

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

(注:以上解释过于简化。要获得更准确的解释,您需要查看CPU制造商为汇编语言编码器和编译器编写者提供的文献。Wikipedia上分支预测的页面是很好的背景。)


然而,在进行此优化时,有一件事需要注意。是否存在a * b != 0会给出错误答案的值?考虑计算乘积导致整数溢出的情况。


更新

你的图表似乎证实了我说的话。

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

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


更新2

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

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

然而,它们都具有适用于ab的所有非负值的优点。

当我们做乘法时,即使一个数是0,那么乘积也是0。在写

    (a*b != 0)

它计算乘积的结果,从而消除从0开始的迭代的前几次出现。因此,当条件为时的比较要少

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

其中每个元素都与0进行比较并求值。因此所需的时间更少。但我相信第二个条件可能会给你更准确的解。

我认为你的基准测试有一些缺陷,可能对推断实际程序没有用处。以下是我的想法:

  • (a|b)!=0(a+b)!=0测试是否要么值为非零,而a != 0 && b != 0(a*b)!=0测试是否这两个值为非零。因此,您不仅仅是在比较算术的时间:如果条件更经常为真,则会导致更多if主体的执行,这也会花费更多的时间。

  • (a+b)!=0将对和为零的正负值做错误的事情,所以你不能在一般情况下使用它,即使它在这里有效。同样,对于a=b=0x80000000 (MIN_VALUE),唯一的设置位将溢出顶部。

  • 类似地,(a*b)!=0将对溢出的值做错误的事情。随机例子:196608 * 327680是0,因为真正的结果恰好能被232整除,所以它的低32位是0,如果它是一个int操作,那么这些位就是你得到的全部。

  • VM将在外层(fraction)循环的前几次运行期间优化表达式,当fraction为0时,当分支几乎从未被使用时。如果你在0.5开始fraction,优化器可能会做不同的事情。

  • 除非VM能够消除这里的一些数组边界检查,否则表达式中还有四个其他分支,这是一个复杂的因素,当试图弄清楚在低级别发生了什么。如果将二维数组拆分为两个平面数组,将nums[0][i]nums[1][i]更改为nums0[i]nums1[i],可能会得到不同的结果。

  • CPU分支预测器检测数据中的短模式,或所有分支被占用或未占用的运行。随机生成的基准测试数据是分支预测者的最坏情况。如果真实世界的数据具有可预测的模式,或者它具有全零和全非零值的长时间运行,则分支的开销可以减少

  • 满足条件后执行的特定代码会影响条件本身计算的性能,因为它会影响诸如是否可以展开循环、哪些CPU寄存器可用以及在计算条件后是否需要重用所获取的nums值等事情。仅仅在基准测试中增加一个计数器并不是真正代码所要做的完美占位符。

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

有很多不确定性,对于这些类型的微优化总是很难确定,因为在一个VM或CPU上更快的技巧可能在另一个VM或CPU上更慢。如果运行的是32位HotSpot JVM,而不是64位版本,请注意它有两种类型:带有"Client"与“服务器”相比,VM具有不同的(较弱的)优化。VM。

如果你能解汇编虚拟机生成的机器码,那就去做,而不是试图猜测它是做什么的!

这里的答案都很好,尽管我有一个想法可能会改善情况。

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

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]是否被求值,但编译器不能确定当你这样做时它不会抛出一个超出范围或空引用。通过将if块减少为简单的boolean,编译器可能足够聪明,可以意识到不必要地计算第二个布尔值不会有负面的副作用。

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

也就是说。我不明白a*b != 0怎么能比(a|b) != 0快。一般来说,整数乘法比位或运算代价更大。但像这样的事情偶尔会变得很奇怪。参见处理器缓存效果图库中的“例7:硬件复杂性”示例。

我知道这个问题很老了,但出于好奇和训练,我用JMH做了一些测试。结果略有不同:

  • 位或运算(a | b != 0)和乘法运算(a * b != 0)是最快的;
  • 普通AND (a!=0 & b!=0)几乎和乘法一样好
  • 短路AND和OR (a!=0 && b!=0!(a!=0 || b!=0))最慢

免责声明:我甚至不是JMH的专家。

在这里的代码,我试图重现有问题的代码,添加了位或:

@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Fork(value = 3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class MultAnd {
    

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MultAnd.class.getSimpleName())
.build();
        

new Runner(opt).run();
}


private static final int size = 50_000_000;
    

@Param({"0.00", "0.10", "0.20", "0.30", "0.40", "0.45",
"0.50", "0.55", "0.60", "0.70", "0.80", "0.90",
"1.00"})
private double fraction;


private int[][] nums;


@Setup
public void setup() {
nums = new int[2][size];
for(int i = 0 ; i < 2 ; i++) {
for(int j = 0 ; j < size ; j++) {
double random = Math.random();
if (random < fraction)
nums[i][j] = 0;
else
nums[i][j] = (int) (random*15 + 1);
}
}
}
    

@Benchmark
public int and() {
int s = 0;
for (int i = 0; i < size; i++) {
if ((nums[0][i]!=0) & (nums[1][i]!=0))
s++;
}
return s;
}
    

@Benchmark
public int andAnd() {
int s = 0;
for (int i = 0; i < size; i++) {
if ((nums[0][i]!=0) && (nums[1][i]!=0))
s++;
}
return s;
}
    

@Benchmark
public int bitOr() {
int s = 0;
for (int i = 0; i < size; i++) {
if ((nums[0][i] | nums[1][i]) != 0)
s++;
}
return s;
}
    

@Benchmark
public int mult() {
int s = 0;
for (int i = 0; i < size; i++) {
if (nums[0][i]*nums[1][i] != 0)
s++;
}
return s;
}
    

@Benchmark
public int notOrOr() {
int s = 0;
for (int i = 0; i < size; i++) {
if (!((nums[0][i]!=0) || (nums[1][i]!=0)))
s++;
}
return s;
}
}

结果是:

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.


Benchmark        (fraction)  Mode  Cnt    Score    Error  Units
MultAnd.and            0.00  avgt   30   33.238 ±  0.883  ms/op
MultAnd.and            0.10  avgt   30   48.011 ±  1.635  ms/op
MultAnd.and            0.20  avgt   30   48.284 ±  1.797  ms/op
MultAnd.and            0.30  avgt   30   47.969 ±  1.463  ms/op
MultAnd.and            0.40  avgt   30   48.999 ±  2.881  ms/op
MultAnd.and            0.45  avgt   30   47.804 ±  1.053  ms/op
MultAnd.and            0.50  avgt   30   48.332 ±  1.990  ms/op
MultAnd.and            0.55  avgt   30   47.457 ±  1.210  ms/op
MultAnd.and            0.60  avgt   30  127.530 ±  3.104  ms/op
MultAnd.and            0.70  avgt   30   92.630 ±  1.471  ms/op
MultAnd.and            0.80  avgt   30   69.458 ±  1.609  ms/op
MultAnd.and            0.90  avgt   30   55.421 ±  1.443  ms/op
MultAnd.and            1.00  avgt   30   30.672 ±  1.387  ms/op
MultAnd.andAnd         0.00  avgt   30   33.187 ±  0.978  ms/op
MultAnd.andAnd         0.10  avgt   30  110.943 ±  1.435  ms/op
MultAnd.andAnd         0.20  avgt   30  177.527 ±  2.353  ms/op
MultAnd.andAnd         0.30  avgt   30  226.247 ±  1.879  ms/op
MultAnd.andAnd         0.40  avgt   30  266.316 ± 18.647  ms/op
MultAnd.andAnd         0.45  avgt   30  258.120 ±  2.638  ms/op
MultAnd.andAnd         0.50  avgt   30  259.727 ±  3.532  ms/op
MultAnd.andAnd         0.55  avgt   30  248.706 ±  1.419  ms/op
MultAnd.andAnd         0.60  avgt   30  229.825 ±  1.256  ms/op
MultAnd.andAnd         0.70  avgt   30  177.911 ±  2.787  ms/op
MultAnd.andAnd         0.80  avgt   30  121.303 ±  2.724  ms/op
MultAnd.andAnd         0.90  avgt   30   67.914 ±  1.589  ms/op
MultAnd.andAnd         1.00  avgt   30   15.892 ±  0.349  ms/op
MultAnd.bitOr          0.00  avgt   30   33.271 ±  1.896  ms/op
MultAnd.bitOr          0.10  avgt   30   35.597 ±  1.536  ms/op
MultAnd.bitOr          0.20  avgt   30   42.366 ±  1.641  ms/op
MultAnd.bitOr          0.30  avgt   30   58.385 ±  0.778  ms/op
MultAnd.bitOr          0.40  avgt   30   85.567 ±  1.678  ms/op
MultAnd.bitOr          0.45  avgt   30   32.152 ±  1.345  ms/op
MultAnd.bitOr          0.50  avgt   30   32.190 ±  1.357  ms/op
MultAnd.bitOr          0.55  avgt   30   32.335 ±  1.384  ms/op
MultAnd.bitOr          0.60  avgt   30   31.910 ±  1.026  ms/op
MultAnd.bitOr          0.70  avgt   30   31.783 ±  0.807  ms/op
MultAnd.bitOr          0.80  avgt   30   31.671 ±  0.745  ms/op
MultAnd.bitOr          0.90  avgt   30   31.329 ±  0.708  ms/op
MultAnd.bitOr          1.00  avgt   30   30.530 ±  0.534  ms/op
MultAnd.mult           0.00  avgt   30   30.859 ±  0.735  ms/op
MultAnd.mult           0.10  avgt   30   33.933 ±  0.887  ms/op
MultAnd.mult           0.20  avgt   30   34.243 ±  0.917  ms/op
MultAnd.mult           0.30  avgt   30   33.825 ±  1.155  ms/op
MultAnd.mult           0.40  avgt   30   34.309 ±  1.334  ms/op
MultAnd.mult           0.45  avgt   30   33.709 ±  1.277  ms/op
MultAnd.mult           0.50  avgt   30   37.699 ±  4.029  ms/op
MultAnd.mult           0.55  avgt   30   36.374 ±  2.948  ms/op
MultAnd.mult           0.60  avgt   30  100.354 ±  1.553  ms/op
MultAnd.mult           0.70  avgt   30   69.570 ±  1.441  ms/op
MultAnd.mult           0.80  avgt   30   47.181 ±  1.567  ms/op
MultAnd.mult           0.90  avgt   30   33.552 ±  0.999  ms/op
MultAnd.mult           1.00  avgt   30   30.775 ±  0.548  ms/op
MultAnd.notOrOr        0.00  avgt   30   15.701 ±  0.254  ms/op
MultAnd.notOrOr        0.10  avgt   30   68.052 ±  1.433  ms/op
MultAnd.notOrOr        0.20  avgt   30  120.393 ±  2.299  ms/op
MultAnd.notOrOr        0.30  avgt   30  177.729 ±  2.438  ms/op
MultAnd.notOrOr        0.40  avgt   30  229.547 ±  1.859  ms/op
MultAnd.notOrOr        0.45  avgt   30  250.660 ±  4.810  ms/op
MultAnd.notOrOr        0.50  avgt   30  258.760 ±  2.190  ms/op
MultAnd.notOrOr        0.55  avgt   30  258.010 ±  1.018  ms/op
MultAnd.notOrOr        0.60  avgt   30  254.732 ±  2.076  ms/op
MultAnd.notOrOr        0.70  avgt   30  227.148 ±  2.040  ms/op
MultAnd.notOrOr        0.80  avgt   30  180.193 ±  4.659  ms/op
MultAnd.notOrOr        0.90  avgt   30  112.212 ±  3.111  ms/op
MultAnd.notOrOr        1.00  avgt   30   33.458 ±  0.952  ms/op

或as graph:
JFreeChart < / p >