java 编译器有多聪明?关于枚举方法的问题

How clever java compiler is? Issues about enum method

考虑这种情况:

样式 1:

static enum Style1{
    FIRE_BALL {
        @Override
        public boolean isCold() {
            return false;
        }
    },ICE_BALL {
        @Override
        public boolean isCold() {
            return true;
        }
    },FIRE_ARROW {
        @Override
        public boolean isCold() {
            return false;
        }
    },ICE_ARROW {
        @Override
        public boolean isCold() {
            return true;
        }
    };
    public abstract boolean isCold();
}

样式 2:

static enum Style2{
    FIRE_BALL,ICE_BALL,FIRE_ARROW,ICE_ARROW;
    public boolean isCold(){
        //return this.toString().contains("ICE")?true:false; //sorry
        return this.toString().contains("ICE");
    }
}

现在,我只想知道冷不冷。所以我要问:

编译器能知道命中注定的结果和常量折叠Style2吗?

如果不是,Style1 应该明显更快,但更冗长。假设这是更复杂的情况并且有更多的组合,例如 BIG_FIRE_SLOW_BALL with isFast(), isBig(), Style1 将以代码块结束。


所以我用 jmh 和 jUnit 做了一些测试:

1.with jmh:

@Benchmark
public boolean testStyle1() {
    return Style1.values()[ThreadLocalRandom.current().nextInt(4)].isCold();
}

@Benchmark
public boolean testStyle2() {
    return Style2.values()[ThreadLocalRandom.current().nextInt(4)].isCold();
}

设置时:

            .warmupIterations(10)
            .measurementIterations(10)
            .threads(8)

Benchmark             Mode  Cnt   Score   Error  Units
EnumTest1.testStyle1  avgt   10  34.057 ± 0.101  ns/op
EnumTest1.testStyle2  avgt   10  36.196 ± 0.453  ns/op

嗯,将线程数设置为 1

            .threads(1)
Benchmark             Mode  Cnt   Score    Error  Units
EnumTest1.testStyle1  avgt   10  34.306 ± 11.692  ns/op
EnumTest1.testStyle2  avgt   10  44.279 ± 11.103  ns/op

因此,编译器似乎无法优化 Style2。

2,使用 jUnit:

private static final int LOOP_TIMES = 100000000;
private static final Random random1=new Random(47);
private static final Random random2=new Random(47);

@Test
public void testStyle1() {
    int cnt = 0;
    for (int i = 0; i < LOOP_TIMES; i++) {
        if(Style1.values()[random1.nextInt(4)].isCold()){
            cnt++;
        }
    }
    System.out.println(cnt);
}

@Test
public void testStyle2() {
    int cnt = 0;
    for (int i = 0; i < LOOP_TIMES; i++) {
        if(Style2.values()[random2.nextInt(4)].isCold()){
            cnt++;
        }
    }
    System.out.println(cnt);
}

结果:

Time:      1       2       3    inverse order   4      5       6
Style1: 3.631s  4.578s  3.754s    Style2     4.131s  5.487s  4.261s  
Style2: 2.559s  4.216s  3.155s    Style1     2.316s  3.977s  4.152s

因此,样式 1 可能更快


但是为什么两个结果很接近,尤其是我同时用jmh做测试的时候?或者我们应该如何处理这个问题?

也许给 Style1 一些字段来存储每个人的结果可以减少冗余。但是我还是觉得不是很满意。希望你们中的一些人能告诉我更多。


非常感谢你们。 @Andy举了一个很好的例子,我在这里补充一下:

enum Style4{
    FIRE_BALL,
    ICE_BALL,
    FIRE_ARROW,
    ICE_ARROW;

    private boolean cold;

    private Style4(){
        this.cold = this.toString().contains("ICE");
    }

    public boolean isCold(){
        return cold;
    }
}

这第四种风格不用说真假就可以工作。

如果使用构造函数,您可以稍微改进 style1 的冗长程度。这会很快,而且(在我看来)更容易阅读。

enum Style1{
    FIRE_BALL(false),
    ICE_BALL(true),
    FIRE_ARROW(false),
    ICE_ARROW(true);

    private final cold;

    private Style1(boolean cold){
        this.cold = cold;
    }

    public boolean isCold(){
        return cold;
    }
}

请注意,这 3 种样式中的 none 可能会成为您代码中的热点。更重要的是编写更易于阅读的代码并在以后根据需要进行性能调整。

Can compiler know the destined results and constant-fold Style2 ?

没有

But why two results are close, especially when I concurrently do the test with jmh? Or how indeed should we deal with this?

正如@JB Nizet 评论的那样,基准测试方法中的大部分时间都花在查找 ThreadLocalRandom 实例和生成下一个随机数上。

我不确定,但我认为每次调用 values() 调用时都必须创建一个新数组。这可以部分解释为什么你在时间上有很多变化。 (JLS 对此没有具体说明,但如果 values() 没有每次都 return 一个新数组,则不受信任的代码有可能更新数组并导致受信任代码出现问题。)

But why two results are close, especially when I concurrently do the test with jmh? Or how indeed should we deal with this?

你真正关心的代码是这样的方法

return true;

不过,你加上这个

if(Style2.values()[random2.nextInt(4)].isCold()){
        cnt++;
    }

方法 Style2.value() 是一种昂贵的方法,它 returns 一个 new 类型 Style2[= 的所有枚举值的数组19=]

方法 nextInt(4) 相当便宜,但包括 % 相对昂贵。

您还有一个 if 条件,这取决于您拥有的 Java 版本可能会很昂贵,因为它会导致分支未命中。 Java 的较新版本使用 CMOV 指令可以更好地处理这个问题。

简而言之,结果与您花费大部分时间做同样的工作大致相同,即不在您认为正在测试的代码中。