我们正在开发对延迟敏感的应用程序,并且已经对各种方法(使用jmh)进行了微基准测试。在对基准测试方法进行微基准测试并对结果满意之后,我实现了最终版本,但发现最终版本比我基准测试的 速度慢 了 3倍 。
罪魁祸首是所实现的方法正在返回一个enum对象而不是一个对象int。这是基准代码的简化版本:
enum
int
@OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Thread) public class ReturnEnumObjectVersusPrimitiveBenchmark { enum Category { CATEGORY1, CATEGORY2, } @Param( {"3", "2", "1" }) String value; int param; @Setup public void setUp() { param = Integer.parseInt(value); } @Benchmark public int benchmarkReturnOrdinal() { if (param < 2) { return Category.CATEGORY1.ordinal(); } return Category.CATEGORY2.ordinal(); } @Benchmark public Category benchmarkReturnReference() { if (param < 2) { return Category.CATEGORY1; } return Category.CATEGORY2; } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(ReturnEnumObjectVersusPrimitiveBenchmark.class.getName()).warmupIterations(5) .measurementIterations(4).forks(1).build(); new Runner(opt).run(); } }
以上基准测试结果:
# VM invoker: C:\Program Files\Java\jdk1.7.0_40\jre\bin\java.exe # VM options: -Dfile.encoding=UTF-8 Benchmark (value) Mode Samples Score Error Units benchmarkReturnOrdinal 3 thrpt 4 1059.898 ± 71.749 ops/us benchmarkReturnOrdinal 2 thrpt 4 1051.122 ± 61.238 ops/us benchmarkReturnOrdinal 1 thrpt 4 1064.067 ± 90.057 ops/us benchmarkReturnReference 3 thrpt 4 353.197 ± 25.946 ops/us benchmarkReturnReference 2 thrpt 4 350.902 ± 19.487 ops/us benchmarkReturnReference 1 thrpt 4 339.578 ± 144.093 ops/us
只需更改函数的返回类型,就可以将性能提高近三倍。
我认为返回枚举对象与整数之间的唯一区别是,一个返回一个64位值(引用),另一个返回一个32位值。我的一位同事猜测,返回枚举会增加额外的开销,因为需要跟踪潜在GC的引用。(但是考虑到枚举对象是静态的最终引用,似乎需要这样做)。
对性能差异的解释是什么?
更新
我在这里共享了maven项目,因此任何人都可以克隆它并运行基准测试。如果有人有时间/兴趣,那么看看别人是否可以复制相同的结果会很有帮助。(我已使用Oracle Java 1.7 JVM的版本在两台不同的计算机Windows 64和Linux 64上进行了复制)。@ZhekaKozlov说,他认为这两种方法没有任何区别。
运行:(克隆存储库后)
mvn clean install java -jar .\target\microbenchmarks.jar function.ReturnEnumObjectVersusPrimitiveBenchmark -i 5 -wi 5 -f 1
TL; DR:您不应将盲目的信任放入任何事物中。
首先,首先:在跳出实验结论之前,验证实验数据很重要。仅仅声称某些东西快3倍或慢3倍是奇怪的,因为您真的需要跟进性能差异的原因,而不仅仅是相信数字。这对于像您这样的纳米基准尤为重要。
其次,实验者应该清楚地了解他们控制什么,而不控制什么。在您的特定示例中,您正在从@Benchmark方法中返回值,但是您可以合理地确定外部的调用者将对原始和引用执行相同的操作吗?如果您问自己这个问题,那么您将意识到您基本上正在测量测试基础结构。
@Benchmark
到了重点。在我的机器(i5-4210U,Linux x86_64,JDK 8u40)上,测试得出:
Benchmark (value) Mode Samples Score Error Units ...benchmarkReturnOrdinal 3 thrpt 5 0.876 ± 0.023 ops/ns ...benchmarkReturnOrdinal 2 thrpt 5 0.876 ± 0.009 ops/ns ...benchmarkReturnOrdinal 1 thrpt 5 0.832 ± 0.048 ops/ns ...benchmarkReturnReference 3 thrpt 5 0.292 ± 0.006 ops/ns ...benchmarkReturnReference 2 thrpt 5 0.286 ± 0.024 ops/ns ...benchmarkReturnReference 1 thrpt 5 0.293 ± 0.008 ops/ns
好的,因此参考测试的速度要慢3倍。但是,等等,它使用了旧的JMH(1.1.1),让我们更新到最新的(1.7.1):
Benchmark (value) Mode Cnt Score Error Units ...benchmarkReturnOrdinal 3 thrpt 5 0.326 ± 0.010 ops/ns ...benchmarkReturnOrdinal 2 thrpt 5 0.329 ± 0.004 ops/ns ...benchmarkReturnOrdinal 1 thrpt 5 0.329 ± 0.004 ops/ns ...benchmarkReturnReference 3 thrpt 5 0.288 ± 0.005 ops/ns ...benchmarkReturnReference 2 thrpt 5 0.288 ± 0.005 ops/ns ...benchmarkReturnReference 1 thrpt 5 0.288 ± 0.002 ops/ns
糟糕,现在它们的速度才差一点。顺便说一句,这也告诉我们测试是受基础架构限制的。好吧,我们可以看看实际发生了什么吗?
如果建立基准测试,并仔细查看究竟调用了什么@Benchmark方法,那么您将看到类似以下内容的信息:
public void benchmarkReturnOrdinal_thrpt_jmhStub(InfraControl control, RawResults result, ReturnEnumObjectVersusPrimitiveBenchmark_jmh l_returnenumobjectversusprimitivebenchmark0_0, Blackhole_jmh l_blackhole1_1) throws Throwable { long operations = 0; long realTime = 0; result.startTime = System.nanoTime(); do { l_blackhole1_1.consume(l_longname.benchmarkReturnOrdinal()); operations++; } while(!control.isDone); result.stopTime = System.nanoTime(); result.realTime = realTime; result.measuredOps = operations; }
那l_blackhole1_1有一个consume“消耗”值的方法(请参阅Blackhole参考资料)。Blackhole.consume具有引用和基元的重载,仅此一项就足以证明性能差异。
l_blackhole1_1
consume
Blackhole
Blackhole.consume
这些方法为何看起来有所不同是有道理的:它们试图针对其参数类型尽可能快。即使我们尝试匹配它们,它们也不一定具有相同的性能特征,因此,更新的JMH的结果更加对称。现在,您甚至可以去-prof perfasm查看为测试生成的代码,并查看性能为何不同,但这超出了本文的重点。
-prof perfasm
如果您真的 想 了解返回原始图元和/或引用在性能方面有何不同,则需要输入细微的性能基准测试的一个 可怕的灰色区域 。例如这样的测试:
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(5) public class PrimVsRef { @Benchmark public void prim() { doPrim(); } @Benchmark public void ref() { doRef(); } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private int doPrim() { return 42; } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private Object doRef() { return this; } }
…这对于基元和引用产生相同的结果:
Benchmark Mode Cnt Score Error Units PrimVsRef.prim avgt 25 2.637 ± 0.017 ns/op PrimVsRef.ref avgt 25 2.634 ± 0.005 ns/op
如上所述,这些测试 需要 跟进结果的原因。在这种情况下,两者生成的代码几乎相同,因此可以解释结果。
prim:
[Verified Entry Point] 12.69% 1.81% 0x00007f5724aec100: mov %eax,-0x14000(%rsp) 0.90% 0.74% 0x00007f5724aec107: push %rbp 0.01% 0.01% 0x00007f5724aec108: sub $0x30,%rsp 12.23% 16.00% 0x00007f5724aec10c: mov $0x2a,%eax ; load "42" 0.95% 0.97% 0x00007f5724aec111: add $0x30,%rsp 0.02% 0x00007f5724aec115: pop %rbp 37.94% 54.70% 0x00007f5724aec116: test %eax,0x10d1aee4(%rip) 0.04% 0.02% 0x00007f5724aec11c: retq
参考:
[Verified Entry Point] 13.52% 1.45% 0x00007f1887e66700: mov %eax,-0x14000(%rsp) 0.60% 0.37% 0x00007f1887e66707: push %rbp 0.02% 0x00007f1887e66708: sub $0x30,%rsp 13.63% 16.91% 0x00007f1887e6670c: mov %rsi,%rax ; load "this" 0.50% 0.49% 0x00007f1887e6670f: add $0x30,%rsp 0.01% 0x00007f1887e66713: pop %rbp 39.18% 57.65% 0x00007f1887e66714: test %eax,0xe3e78e6(%rip) 0.02% 0x00007f1887e6671a: retq
[讽刺]看看有多容易![/讽刺]
模式是:问题越简单,您就需要做出更多的努力才能得出合理而可靠的答案。