通常的做法是将常量空数组返回值提取为静态常量。像这里:
public class NoopParser implements Parser {
private static final String[] EMPTY_ARRAY = new String[0];
@Override public String[] supportedSchemas() {
return EMPTY_ARRAY;
}
// ...
}
可能这是出于性能原因而完成的,因为每次调用该方法时直接返回new String[0]
会创建一个新的数组对象 - 但它真的会吗?
我一直想知道这样做是否真的有可衡量的表现优势,或者它是否只是过时的民间智慧。空数组是不可变的。 VM是否无法将所有空String
阵列都卷成一个?虚拟机能否基本免费new String[0]
?
将此做法与返回空字符串进行对比:我们通常非常乐意写return "";
,而不是return EMPTY_STRING;
。
答案 0 :(得分:5)
我使用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) {
}
环境(在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
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
使用常数几乎快了两倍。
关闭EliminateAllocations
减慢了一点点。
答案 1 :(得分:2)
VM是否无法将所有空字符串数组卷成一个?
它不能这样做,因为不同的空数组需要与{{1}}进行比较。只有程序员才能进行这种优化。
将此练习与返回空字符串进行对比:我们通常非常乐意写{{1}}。
对于字符串,不要求不同的字符串文字产生不同的字符串。在我知道的每种情况下,{{1}}的两个实例将生成相同的字符串对象,但也许有一些奇怪的情况与类加载器不会发生。
答案 2 :(得分:2)
我最感兴趣的是这两个习语在实际的现实世界中的实际表现差异。我没有微基准测试的经验(可能不是这个问题的正确工具),但我还是试了一下。
此基准测试模拟了一种更为典型的“现实”设置。返回的数组只是查看然后丢弃。没有引用,没有参考平等的要求。
一个界面,两个实现:
public interface Parser {
String[] supportedSchemas();
void parse(String s);
}
public class NoopParserStaticArray implements Parser {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
@Override public String[] supportedSchemas() {
return EMPTY_STRING_ARRAY;
}
@Override public void parse(String s) {
s.codePoints().count();
}
}
public class NoopParserNewArray implements Parser {
@Override public String[] supportedSchemas() {
return new String[0];
}
@Override public void parse(String s) {
s.codePoints().count();
}
}
JMH基准:
import org.openjdk.jmh.annotations.Benchmark;
public class EmptyArrayBenchmark {
private static final Parser NOOP_PARSER_STATIC_ARRAY = new NoopParserStaticArray();
private static final Parser NOOP_PARSER_NEW_ARRAY = new NoopParserNewArray();
@Benchmark
public void staticEmptyArray() {
Parser parser = NOOP_PARSER_STATIC_ARRAY;
for (String schema : parser.supportedSchemas()) {
parser.parse(schema);
}
}
@Benchmark
public void newEmptyArray() {
Parser parser = NOOP_PARSER_NEW_ARRAY;
for (String schema : parser.supportedSchemas()) {
parser.parse(schema);
}
}
}
我的机器上的结果,Java 1.8.0_51(HotSpot VM):
Benchmark Mode Cnt Score Error Units
EmptyArrayBenchmark.staticEmptyArray thrpt 60 3024653836.077 ± 37006870.221 ops/s
EmptyArrayBenchmark.newEmptyArray thrpt 60 3018798922.045 ± 33953991.627 ops/s
EmptyArrayBenchmark.noop thrpt 60 3046726348.436 ± 5802337.322 ops/s
在这种情况下,两种方法之间没有显着差异。事实上,它们与无操作情况无法区分:显然JIT编译器认识到返回的数组总是为空并且完全优化了循环!
将parser.supportedSchemas()
插入黑洞而不是在其上循环,使静态数组实例接近〜30%的优势。但他们肯定有同样的规模:
Benchmark Mode Cnt Score Error Units
EmptyArrayBenchmark.staticEmptyArray thrpt 60 338971639.355 ± 738069.217 ops/s
EmptyArrayBenchmark.newEmptyArray thrpt 60 266936194.767 ± 411298.714 ops/s
EmptyArrayBenchmark.noop thrpt 60 3055609298.602 ± 5694730.452 ops/s
也许最后答案是通常的“它取决于”。我有一种预感,在许多实际情况中, 性能在分解数组创建方面的好处并不重要。
我认为可以公平地说
然后直接返回new String[0]
就好了。
就个人而言,我喜欢return new String[0];
的表现力和简洁性,而不必引入额外的静态字段。
有些奇怪的巧合,在我写这篇文章一个月之后,一位真正的表演工程师调查了这个问题:请参阅AlexeyShipilёv博客文章“古代智慧阵列”中的this section:
正如预期的那样,可以在非常小的集合大小上观察到唯一的影响,这仅比
new Foo[0]
略有改善。这种改进似乎不足以证明缓存在宏伟的计划中阵列。作为一个微小的微优化,它可能在一些严格的代码中有意义,但我不会在乎。
解决了这个问题。我会把刻度标记给Alexey。
答案 3 :(得分:0)
我会说到性能优势,即使使用常数更快,但实际上并不相关;因为除了返回空数组之外,软件可能会花费更多时间做其他事情。如果总运行时间是偶数小时,则在创建阵列上花费额外的几秒钟并不意味着什么。按照相同的逻辑,内存消耗也不相关。
我能想到这样做的唯一原因是可读性。