【问题标题】:Is there a performance difference between multiple "if" statements vs. "if else if" for mutually exclusive conditions?对于互斥条件,多个“if”语句与“if else if”之间是否存在性能差异?
【发布时间】:2019-09-21 11:46:05
【问题描述】:

我很好奇 Java 如何对具有互斥条件的多个“if”语句进行优化,但我没有自己分析它的知识。题基本就是这个题的Java版Performance difference of "if if" vs "if else if"

我已经看到 ifreturn 的语句得到了回答,但这个问题是针对具有互斥条件但不返回的 if 语句。

1.多个 if 语句

if (x == 0) doSomething();
if (x == 2) doSomething();
if (x == 5) doSomething();

2。链式 If-else 语句

if (x == 0) doSomething();
else if (x == 2) doSomething();
else if (x == 5) doSomething();

问题
#1 和 #2 执行相同的后编译吗?
(另外:如果是这样,Java 可以优化多复杂的条件?)

【问题讨论】:

  • 它们确实生成相同的 Java 字节码,您可以在 javabytes.io 进行验证。但是,我怀疑它们会生成相同的 JITted 代码。
  • @BrennanVincent 根据这两种方法的时序,即使使用 JIT 编译器和分支预测,似乎 if-else 也更快。看我的回答。

标签: java optimization


【解决方案1】:

嗯,只有适当的 JMH 测试才能证明某种方法的速度有多快。当然,需要注意的是,如果您真的想知道为什么这些数字就是它们的样子,那么您还应该了解底层的机器代码。我把这留给你,只是在这个测试中展示数字,只是稍微向你展示一些细节。

package com.so;

import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

@Warmup(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
public class IfElseCompare {

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(IfElseCompare.class.getName())
            .jvmArgs("-ea")
            .build();

        new Runner(opt).run();
    }

    private int resolveValueMultipleIfs(IfElseExecutionPlan plan) {

        int x = -1;

        if (plan.value() == 0) {
            x = 0;
        }

        if (plan.value() == 1) {
            x = 1;
        }

        if (plan.value() == 2) {
            x = 2;
        }

        assert x != -1;
        return x;
    }

    private int resolveValueIfElse(IfElseExecutionPlan plan) {
        int x = -1;
        if (plan.value() == 0) {
            x = 0;
        } else if (plan.value() == 1) {
            x = 1;
        } else if (plan.value() == 2) {
            x = 2;
        }

        assert x != -1;
        return x;
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(1)
    public int multipleIf(IfElseExecutionPlan plan) {
        return resolveValueMultipleIfs(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(1)
    public int ifElse(IfElseExecutionPlan plan) {
        return resolveValueIfElse(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-Xint")
    public int multipleIfsfNoJIT(IfElseExecutionPlan plan) {
        return resolveValueMultipleIfs(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-Xint")
    public int ifElseNoJIT(IfElseExecutionPlan plan) {
        return resolveValueIfElse(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:-TieredCompilation")
    public int multipleIfsC2Only(IfElseExecutionPlan plan) {
        return resolveValueMultipleIfs(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:-TieredCompilation")
    public int ifElseC2Only(IfElseExecutionPlan plan) {
        return resolveValueIfElse(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:TieredStopAtLevel=1")
    public int multipleIfsC1Only(IfElseExecutionPlan plan) {
        return resolveValueMultipleIfs(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:TieredStopAtLevel=1")
    public int ifElseC1Only(IfElseExecutionPlan plan) {
        return resolveValueIfElse(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1,
        jvmArgsAppend = {
            "-XX:+UnlockExperimentalVMOptions",
            "-XX:+EagerJVMCI",
            "-Dgraal.ShowConfiguration=info",
            "-XX:+UseJVMCICompiler",
            "-XX:+EnableJVMCI"
        })
    public int multipleIfsGraalVM(IfElseExecutionPlan plan) {
        return resolveValueMultipleIfs(plan);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1,
        jvmArgsAppend = {
            "-XX:+UnlockExperimentalVMOptions",
            "-XX:+EagerJVMCI",
            "-Dgraal.ShowConfiguration=info",
            "-XX:+UseJVMCICompiler",
            "-XX:+EnableJVMCI"
        })
    public int ifElseGraalVM(IfElseExecutionPlan plan) {
        return resolveValueIfElse(plan);
    }
}

结果如下:

IfElseCompare.ifElse              avgt    2    2.826          ns/op
IfElseCompare.multipleIf          avgt    2    3.061          ns/op

IfElseCompare.ifElseC1Only        avgt    2    3.927          ns/op
IfElseCompare.multipleIfsC1Only   avgt    2    4.397          ns/op

IfElseCompare.ifElseC2Only        avgt    2    2.507          ns/op
IfElseCompare.multipleIfsC2Only   avgt    2    2.428          ns/op

IfElseCompare.ifElseGraalVM       avgt    2    2.587          ns/op
IfElseCompare.multipleIfsGraalVM  avgt    2    2.854          ns/op

IfElseCompare.ifElseNoJIT         avgt    2  232.418          ns/op   
IfElseCompare.multipleIfsfNoJIT   avgt    2  303.371          ns/op

如果反编译带有多个if条件的版本:

  0x000000010cf8542c: test   %esi,%esi
  0x000000010cf8542e: je     0x000000010cf8544f             ;*ifne {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - com.so.IfElseCompare::resolveValueMultipleIfs@3 (line 21)

  0x000000010cf85430: cmp    $0x1,%esi
  0x000000010cf85433: je     0x000000010cf8545e             ;*if_icmpne {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - com.so.IfElseCompare::resolveValueMultipleIfs@10 (line 25)

  0x000000010cf85435: cmp    $0x2,%esi
  0x000000010cf85438: je     0x000000010cf8546e             ;*if_icmpne {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - com.so.IfElseCompare::resolveValueMultipleIfs@17 (line 29)

一系列cmp/je - 比较和如果相等就跳转,嗯,非常期待。

if/else的反编译代码是一样的东西(我让你反编译你亲眼看看);使用(java-12)生成的ASM代码:

java -XX:+UnlockDiagnosticVMOptions  
     -XX:CICompilerCount=2 
     -XX:-TieredCompilation  
     "-XX:CompileCommand=print,com/so/IfElseCompare.resolveValueMultipleIfs"  
     com.so.IfElseCompare

【讨论】:

  • 既然机器码看起来是一样的,那么大约 10% 的时间变化可能只是噪音吗?
  • @Jeck 很可能,是的。我可能会重新运行它们更长的时间和更多的堆,这可能会使结果稍微均匀。这仍然是纳秒级的差异......
【解决方案2】:

有一点不同,虽然很小。关键问题是该过程的任何步骤是否由足够智能的软件完成以推断如果x==0,则x==2x==5 必须为假。

在 Java 字节码级别,它们通常会产生不同的结果。编译器没有义务足够聪明地分析差异。 (Eugene 对相关问题的answer 表明 Sun 的 Java 12 编译器确实足够聪明,可以在某些情况下为您进行优化)

及时编译器往往比较激进。他们更有可能意识到代码只能流经三个分支之一并将其优化掉。但这仍然是一个依赖于工具的声明。 Java 语言本身将它们视为不同的。

现在,实际上,除非您进行非常紧密的循环,否则这一点都不重要。优化中的第一条规则是“配置文件,然后优化”。在至少 99% 的情况下,没有理由优化这些细节。

具体来说,在您给出的示例中,即使编译器和 JIT 无法为您优化代码,性能成本也可以忽略不计。在“平均”CPU 上,成功预测的分支大约是函数调用成本的十分之一,因此您在这些分支上调用doSomething() 的事实将使成本相形见绌。如果额外的调用导致一些额外的分支错误预测,您可能会看到更糟糕的效果,但没有什么比调用函数更昂贵的了。

现在,假设 doSomething() 实际上是像 x += 1 这样快速的占位符,那么您需要进行分析以确定它是否正确。

所以,我的建议是写if/if/ifif/else if/else if,以正确为准。对于您要使用的逻辑,哪个最有意义就是正确的答案。如果这打算成为一个只采用一条路径的分支,我建议else if。如果这是一个函数可能在未来执行许多分支的情况,但恰好当前分支列表是互斥的,请使用if/if/if 向读者传达预期的结果。

然后是个人资料。始终配置文件。如果你发现这个函数是一个热点,那么考虑一下 if 语句是否很昂贵。

顺便说一句,编译器很难证明他们可以将if 转换为else if。它必须对 x 进行分析,看看是否有可能被另一个线程修改。如果它是一个局部变量,其他线程不能修改它。但是,如果它是成员变量,则另一个线程可能会在您的 if/if/if 块中间修改 x,从而导致它采用两条路径。你可能知道没有其他人会像这样修改x,但是编译器必须在进行这样的优化之前证明它,或者至少证明它所写的内容与它的规则实现是一致的用于 Java 的内存模型。

【讨论】:

  • 对于 random 数据,反编译代码为 exactly the same
  • @Eugene 对于您运行的测试,它们是相同的。结果是特定于编译器的并且取决于x 所在的位置。在您的测试中,x 始终是一个局部变量。在这种情况下,进行闭包分析以证明 x 不能被另一个线程修改很容易,因为 Java 保证没有其他线程可以看到线程的局部变量。如果 x 在其他地方,那可能不是真的(并且 OP 的代码段没有指定 x 在哪里)
  • @Eugene 我已经编辑指出您的 Java 12 版本足够智能,可以在这种情况下进行优化。
  • 我不明白你的意思,阅读x 会如何影响 if/else 并且 当然 这是特定于编译器的。我也不知道为什么要把内存模型带入这个讨论。
  • @Eugene 从 if/if 到 if/else 的转换只有在编译器可以假设 x 的值在后续测试之间没有变化时才有效。这可能来自 doSomething() 在编译时无法完全检查,或者可能来自编译器必须处理 Java 内存模型定义的其他线程。
【解决方案3】:

没有什么能比得上老式的计时测试:

long total = 0;
long startTime;
long endTime;

for (int j = 0; j < 10; j++) {
    startTime = System.currentTimeMillis();

    for (int i = 0; i < 100000000; i++) {
        if (i % 3 == 0) total += 1;
        if (i % 3 == 1) total += 2;
        if (i % 3 == 2) total += 3;
    }

    endTime = System.currentTimeMillis();
    System.out.println("If only: " + (endTime - startTime));

    startTime = System.currentTimeMillis();

    for (int i = 0; i < 100000000; i++) {
        if (i % 3 == 0) total += 1;
        else if (i % 3 == 1) total += 2;
        else if (i % 3 == 2) total += 3;
    }

    endTime = System.currentTimeMillis();
    System.out.println("If-else: " + (endTime - startTime));
}
System.out.println(total);

('total' 值是防止编译器删除整个循环所必需的!)

输出:

If only: 215
If-else: 137
If only: 214
If-else: 121
If only: 210
If-else: 120
If only: 211
If-else: 120
If only: 211
If-else: 121
If only: 210
If-else: 121
If only: 210
If-else: 121
If only: 211
If-else: 120
If only: 211
If-else: 120
If only: 211
If-else: 121
3999999980

正如我们所见,即使 if 条件显然是互斥的,if-else 块的运行速度也会明显加快。由于两个循环所用的时间长度不同,因此每个循环的编译代码必须不同。显然编译器没有对此进行优化。 JIT 或 CPU 也不完全分支预测。还是有很大区别的。

我的建议:尽可能使用 If-else

编辑:我还尝试交换两个循环并得到相同的结果。 if-else if 更快。

编辑 2: 我在整个测试中添加了一个 for 循环,以消除初始化或预热中的任何差异。结果是一样的。

【讨论】:

  • 我在回答中提到,只有在 doSomething() 很简单的情况下,这种性能打击才重要。我会说你选择的测试是“简单”中的权威测试,所以这个性能打击确实是它所能得到的最糟糕的!
  • 我会用大量的盐来处理这些结果。从这些类型的时序测试中得出可靠的结论非常困难。
  • @sprinter 关于像这样简单计时测试的难度,我也尝试交换两个循环,得到了相同的结果。 if-else 仍然表现得更好。
  • 这回答了这个问题,因为它表明最终的机器代码是不一样的(与可以由 JIT 编译器进一步优化的字节码相反),至少对于特定的 JIT 编译器实现@DanielWilliams 正在使用
  • @DanielWilliams 不幸的是,它不仅仅是交换循环。任何语言的微基准测试都很难,但在 Java 中尤其困难,因为您的代码范围之外有如此多的环境控制。有关详细信息,请参阅stackoverflow.com/questions/504103/…
【解决方案4】:

让我告诉你条件运算符“if()”是如何工作的。当您编写 if() 语句时,它会检查您在这些“()”中提供的条件的真实性。如果条件失败,则编译器会寻找可在 if() 条件失败时使用的替代语句或代码块。现在对于这个替代内容,我们使用“else”块。

现在根据您的问题,答案很容易理解。这两种方式有很大的不同。

1)。多个 If 语句

if (x == 0) doSomething();
if (x == 2) doSomething();
if (x == 5) doSomething();

在上面的代码中,无论是否满足任何条件,编译器都会解析所有 if 语句。因为它们是单独使用的,没有任何替代部分。

2)。链式 If-else 语句

if (x == 0) doSomething();
else if (x == 2) doSomething();
else if (x == 5) doSomething();

现在在上面的代码中有一个主要的条件检查器(x==0),如果它失败了,那么还有其他的选择,所以编译器会检查它们,直到找到满意的解决方案。

性能问题

在第一种情况下,编译器必须检查每个条件,因为它们都是独立的,这可能需要更多时间。但在第二种情况下,它只会在 if() 语句不满足条件时编译“else if”部分。所以是的,在性能方面它们之间可能会有一点差异。

我希望它有所帮助。

【讨论】:

    猜你喜欢
    • 2015-07-08
    • 2021-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-01
    • 1970-01-01
    • 2013-09-01
    • 1970-01-01
    相关资源
    最近更新 更多