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

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

我正在 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 快:

我很想知道为什么。任何人都可以阐明一下吗?是编译器还是硬件层面的?

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

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

更新

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

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

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

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

注意:在我考虑所有这些的上下文中,int 溢出不是问题,但在一般上下文中这绝对是一个重要的考虑因素。

CPU:英特尔酷睿 i7-3610QM @ 2.3GHz

Java版本:1.8.0_45
Java(TM) SE 运行时环境(build 1.8.0_45-b14)
Java HotSpot(TM) 64 位服务器 VM(内部版本 25.45-b02,混合模式)

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

Is it the compiler or is it at the hardware level?

后者,我认为:

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

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

  if (a * b != 0)

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

如果硬件级分支预测无效,乘法可能比第二个条件分支更快。随着比率的增加......分支预测变得越来越不有效。

条件分支较慢的原因是它们导致指令执行流水线停滞。分支预测是通过预测分支将走哪条路并据此推测选择下一条指令来避免停顿。如果预测失败,则加载另一个方向的指令时会有延迟。

(注:以上解释过于简单,更准确的解释需要查看CPU厂商为汇编语言编码员和编译器编写者提供的文献。[=上的维基百科页面19=] 是很好的背景。)


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


更新

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

  • 在条件分支a * b != 0的情况下也有"branch prediction"的效果,这在图表中表现出来。

  • 如果将曲线投影到 X 轴上超过 0.9,则看起来 1) 它们将在大约 1.0 和 2) 的 Y 值处相交X = 0.0.


更新 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 测试 both 是否非零。所以你不只是比较算术的时间:如果条件更频繁地为真,它会导致更多的执行 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 分支预测器检测数据中的短模式,或者所有分支的运行被采用或不被采用。您随机生成的基准数据是 worst-case scenario for a branch predictor。如果现实世界的数据具有可预测的模式,或者它具有长时间的全零值和全非零值,则分支的成本可能会降低 很多

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

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

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

如果可以disassemble the machine code generated by the 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 块简化为简单的布尔值,编译器可能足够聪明,可以意识到不必要地评估第二个布尔值不会产生负面影响。

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

话虽如此。我不明白 a*b != 0 怎么会比 (a|b) != 0 快。通常整数乘法比按位或更昂贵。但是像这样的事情偶尔会变得很奇怪。例如,参见 Gallery of Processor Cache Effects 中的 "Example 7: Hardware complexities" 示例。

我知道这个问题很老了,但是为了好奇心和培训,我使用 JMH 做了一些测试。结果略有不同:

  • bit-wise OR (a | b != 0) 和乘法 (a * b != 0) 是最快的;
  • 普通 AND (a!=0 & b!=0) 几乎和乘法一样好
  • short-circuiting AND 和 OR(a!=0 && b!=0!(a!=0 || b!=0) 最慢

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

这里是代码,我试图重现有问题的代码,添加了 bit-wise 或者:

@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

或如图: