静态空数组实例的性能优势

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;.

Ada*_*zyk 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) {
}
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会使速度变慢。

  • 即使使用JMH,这也是一个非常不公平的基准。1)手动编写基准循环是一个错误;JMH为您做到。2)黑洞不消耗结果。3)测试无法比较:`testNew`消耗大量内存,因为它将所有中间结果保存在一起;在实际实践中,这不是通常的情况。4)测试的编写方式使JVM无法应用分配消除优化,但是这种优化对于处理从方法返回的短期对象至关重要(请参阅http://stackoverflow.com/questions/33765687/)。 (3认同)

glt*_*lts 3

我最感兴趣的是这两个习惯用法在实际情况下的实际性能差异。我没有微基准测试的经验(它可能不是解决此类问题的正确工具),但无论如何我还是尝试了一下。

\n\n

该基准测试模拟了一个更典型的“现实”设置。返回的数组只是被查看然后被丢弃。没有引用闲置,没有引用相等的要求。

\n\n

一个接口,两种实现:

\n\n
public interface Parser {\n    String[] supportedSchemas();\n    void parse(String s);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n\n\n
public 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}\n
Run Code Online (Sandbox Code Playgroud)\n\n\n\n
public 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}\n
Run Code Online (Sandbox Code Playgroud)\n\n

JMH 基准测试:

\n\n
import 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}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我的机器 Java 1.8.0_51 (HotSpot VM) 上的结果:

\n\n
Benchmark                              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\n
Run Code Online (Sandbox Code Playgroud)\n\n

在这种情况下,两种方法没有显着差异。事实上,它们与无操作的情况没有区别:显然 JIT 编译器认识到返回的数组始终为空,并完全优化了循环!

\n\n

通过管道parser.supportedSchemas()进入黑洞而不是在黑洞上循环,静态数组实例方法具有约 30% 的优势。但它们的大小绝对相同:

\n\n
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\n
Run Code Online (Sandbox Code Playgroud)\n\n

也许最终的答案是通常的“视情况而定”。我有预感,在许多实际场景中,分解数组创建的性能优势并不显着。

\n\n

我认为这样说是公平的

\n\n
    \n
  • 如果方法契约让您可以自由地每次返回一个新的空数组实例,并且
  • \n
  • 除非您需要防范有问题或病态的使用模式和/或以理论上的最大性能为目标,
  • \n
\n\n

然后new String[0]直接返回就可以了。

\n\n

就我个人而言,我喜欢它的表现力和简洁性return new String[0];,而且不需要引入额外的静态字段。

\n\n
\n\n

出于某种奇怪的巧合,在我写这篇文章一个月后,一位真正的性能工程师调查了这个问题:请参阅Alexey Shipil\xd1\x91v\ 的博客文章“Arrays of Wisdom of theAncients”中的这一部分:

\n\n
\n

正如预期的那样,唯一的影响可以在非常小的集合大小上观察到,并且这只是相对于的微小改进new Foo[0]从总体上看,这种改进似乎并不能证明缓存数组是合理的。作为一个微小的微优化,它在一些紧凑的代码中可能有意义,但我不会\xe2\x80\x99否则关心。

\n
\n\n

这样就解决了。我将把勾号献给阿列克谢。

\n