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
指令可以更好地处理这个问题。
简而言之,结果与您花费大部分时间做同样的工作大致相同,即不在您认为正在测试的代码中。
考虑这种情况:
样式 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
指令可以更好地处理这个问题。
简而言之,结果与您花费大部分时间做同样的工作大致相同,即不在您认为正在测试的代码中。