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

时间:2015-12-15 23:14:29

标签: java arrays performance

通常的做法是将常量空数组返回值提取为静态常量。像这里:

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;

4 个答案:

答案 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) {
}

Full source code

环境(在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)

我会说到性能优势,即使使用常数更快,但实际上并不相关;因为除了返回空数组之外,软件可能会花费更多时间做其他事情。如果总运行时间是偶数小时,则在创建阵列上花费额外的几秒钟并不意味着什么。按照相同的逻辑,内存消耗也不相关。

我能想到这样做的唯一原因是可读性。