glt*_*lts 7 java arrays performance
通常的做法是将常量空数组返回值提取为静态常量.像这儿:
public class NoopParser implements Parser {
private static final String[] EMPTY_ARRAY = new String[0];
@Override public String[] supportedSchemas() {
return EMPTY_ARRAY;
}
// ...
}
Run Code Online (Sandbox Code Playgroud)
可能这是出于性能原因而完成的,因为new String[0]每次调用该方法时直接返回会创建一个新的数组对象 - 但它真的会吗?
我一直想知道这样做是否真的有可衡量的性能优势,或者它是否只是过时的民间智慧.空数组是不可变的.VM是否无法将所有空String数组卷成一个?VM new String[0]基本上没有成本吗?
将这种做法与返回一个空字符串进行对比:我们通常很乐意写return "";,而不是return EMPTY_STRING;.
我使用JMH对它进行了基准测试:
private static final String[] EMPTY_STRING_ARRAY = new String[0];
@Benchmark
public void testStatic(Blackhole blackhole) {
blackhole.consume(EMPTY_STRING_ARRAY);
}
@Benchmark
@Fork(jvmArgs = "-XX:-EliminateAllocations")
public void testStaticEliminate(Blackhole blackhole) {
blackhole.consume(EMPTY_STRING_ARRAY);
}
@Benchmark
public void testNew(Blackhole blackhole) {
blackhole.consume(new String[0]);
}
@Benchmark
@Fork(jvmArgs = "-XX:-EliminateAllocations")
public void testNewEliminate(Blackhole blackhole) {
blackhole.consume(new String[0]);
}
@Benchmark
public void noop(Blackhole blackhole) {
}
Run Code Online (Sandbox Code Playgroud)
环境(见后java -jar target/benchmarks.jar -f 1):
# JMH 1.11.2 (released 51 days ago)
# VM version: JDK 1.7.0_75, VM 24.75-b04
# VM invoker: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
Run Code Online (Sandbox Code Playgroud)
EliminateAllocations默认情况下处于开启状态(在之后可见java -XX:+PrintFlagsFinal -version | grep EliminateAllocations)。
结果:
Benchmark Mode Cnt Score Error Units
MyBenchmark.testNewEliminate thrpt 20 95912464.879 ± 3260948.335 ops/s
MyBenchmark.testNew thrpt 20 103980230.952 ± 3772243.160 ops/s
MyBenchmark.testStaticEliminate thrpt 20 206849985.523 ± 4920788.341 ops/s
MyBenchmark.testStatic thrpt 20 219735906.550 ± 6162025.973 ops/s
MyBenchmark.noop thrpt 20 1126421653.717 ± 8938999.666 ops/s
Run Code Online (Sandbox Code Playgroud)
使用常数几乎快了两倍。
关闭电源EliminateAllocations会使速度变慢。
我最感兴趣的是这两个习惯用法在实际情况下的实际性能差异。我没有微基准测试的经验(它可能不是解决此类问题的正确工具),但无论如何我还是尝试了一下。
\n\n该基准测试模拟了一个更典型的“现实”设置。返回的数组只是被查看然后被丢弃。没有引用闲置,没有引用相等的要求。
\n\n一个接口,两种实现:
\n\npublic interface Parser {\n String[] supportedSchemas();\n void parse(String s);\n}\nRun Code Online (Sandbox Code Playgroud)\n\n\n\npublic class NoopParserStaticArray implements Parser {\n private static final String[] EMPTY_STRING_ARRAY = new String[0];\n\n @Override public String[] supportedSchemas() {\n return EMPTY_STRING_ARRAY;\n }\n\n @Override public void parse(String s) {\n s.codePoints().count();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n\n\npublic class NoopParserNewArray implements Parser {\n @Override public String[] supportedSchemas() {\n return new String[0];\n }\n\n @Override public void parse(String s) {\n s.codePoints().count();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\nJMH 基准测试:
\n\nimport org.openjdk.jmh.annotations.Benchmark;\n\npublic class EmptyArrayBenchmark {\n private static final Parser NOOP_PARSER_STATIC_ARRAY = new NoopParserStaticArray();\n private static final Parser NOOP_PARSER_NEW_ARRAY = new NoopParserNewArray();\n\n @Benchmark\n public void staticEmptyArray() {\n Parser parser = NOOP_PARSER_STATIC_ARRAY;\n for (String schema : parser.supportedSchemas()) {\n parser.parse(schema);\n }\n }\n\n @Benchmark\n public void newEmptyArray() {\n Parser parser = NOOP_PARSER_NEW_ARRAY;\n for (String schema : parser.supportedSchemas()) {\n parser.parse(schema);\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n我的机器 Java 1.8.0_51 (HotSpot VM) 上的结果:
\n\nBenchmark Mode Cnt Score Error Units\nEmptyArrayBenchmark.staticEmptyArray thrpt 60 3024653836.077 \xc2\xb1 37006870.221 ops/s\nEmptyArrayBenchmark.newEmptyArray thrpt 60 3018798922.045 \xc2\xb1 33953991.627 ops/s\nEmptyArrayBenchmark.noop thrpt 60 3046726348.436 \xc2\xb1 5802337.322 ops/s\nRun Code Online (Sandbox Code Playgroud)\n\n在这种情况下,两种方法没有显着差异。事实上,它们与无操作的情况没有区别:显然 JIT 编译器认识到返回的数组始终为空,并完全优化了循环!
\n\n通过管道parser.supportedSchemas()进入黑洞而不是在黑洞上循环,静态数组实例方法具有约 30% 的优势。但它们的大小绝对相同:
Benchmark Mode Cnt Score Error Units\nEmptyArrayBenchmark.staticEmptyArray thrpt 60 338971639.355 \xc2\xb1 738069.217 ops/s\nEmptyArrayBenchmark.newEmptyArray thrpt 60 266936194.767 \xc2\xb1 411298.714 ops/s\nEmptyArrayBenchmark.noop thrpt 60 3055609298.602 \xc2\xb1 5694730.452 ops/s\nRun Code Online (Sandbox Code Playgroud)\n\n也许最终的答案是通常的“视情况而定”。我有预感,在许多实际场景中,分解数组创建的性能优势并不显着。
\n\n我认为这样说是公平的
\n\n然后new String[0]直接返回就可以了。
就我个人而言,我喜欢它的表现力和简洁性return new String[0];,而且不需要引入额外的静态字段。
出于某种奇怪的巧合,在我写这篇文章一个月后,一位真正的性能工程师调查了这个问题:请参阅Alexey Shipil\xd1\x91v\ 的博客文章“Arrays of Wisdom of theAncients”中的这一部分:
\n\n\n\n\n正如预期的那样,唯一的影响可以在非常小的集合大小上观察到,并且这只是相对于的微小改进
\nnew Foo[0]。从总体上看,这种改进似乎并不能证明缓存数组是合理的。作为一个微小的微优化,它在一些紧凑的代码中可能有意义,但我不会\xe2\x80\x99否则关心。
这样就解决了。我将把勾号献给阿列克谢。
\n| 归档时间: |
|
| 查看次数: |
691 次 |
| 最近记录: |