Java 静态空阵列实例的性能优势

Java 静态空阵列实例的性能优势,java,arrays,performance,Java,Arrays,Performance,将常量空数组返回值提取为静态常量似乎是常见的做法。就像这里: public class NoopParser implements Parser { private static final String[] EMPTY_ARRAY = new String[0]; @Override public String[] supportedSchemas() { return EMPTY_ARRAY; } // ... } 这可能是出于性能原因,

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

public class NoopParser implements Parser {
    private static final String[] EMPTY_ARRAY = new String[0];

    @Override public String[] supportedSchemas() {
        return EMPTY_ARRAY;
    }

    // ...
}
这可能是出于性能原因,因为每次调用该方法时直接返回
新字符串[0]
将创建一个新的数组对象,但它真的会这样吗

我一直在想,这样做是否真的有可测量的性能优势,或者这只是过时的民间智慧。空数组是不可变的。虚拟机是否无法将所有空
字符串
数组合并为一个?虚拟机能否基本上免费创建
新字符串[0]

将这种做法与返回空字符串进行对比:我们通常非常乐意编写
return”“
,而不是
返回空字符串

我使用以下方法对其进行了基准测试:

环境(见
java-jar-target/benchmarks.jar-f1
):

使用常数几乎快了两倍

关闭
EliminateAllocations
会稍微降低速度

VM是否无法将所有空字符串数组合并为一个

它不能这样做,因为不同的空数组需要与
==
进行比较。只有程序员才能进行这种优化

将这种做法与返回空字符串进行对比:我们通常非常乐意编写
return”“


对于字符串,不要求不同的字符串文本生成不同的字符串。在我所知道的每一种情况下,
的两个实例都会生成相同的字符串对象,但可能在类加载器中会出现一些奇怪的情况,而这种情况不会发生。

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


我能想到的唯一原因是可读性

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

这个基准模拟了一个更典型的“现实”设置。只需查看返回的数组,然后将其丢弃。没有挂起的引用,没有引用相等的要求

一个接口,两个实现:

public interface Parser {
    String[] supportedSchemas();
    void parse(String s);
}
以及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)上的结果:

在这种情况下,两种方法之间没有显著差异。事实上,它们与无操作的情况是无法区分的:显然,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
也许最后的答案是通常的“视情况而定”。我有一种预感,在许多实际场景中,分解阵列创建的性能好处并不显著。

我认为这样说是公平的

  • 如果方法契约允许您每次返回一个新的空数组实例,并且
  • 除非您需要防范有问题或病态的使用模式和/或追求理论上的最高性能
然后直接返回
新字符串[0]
就可以了

就我个人而言,我喜欢
returnnewstring[0]的表达性和简洁性并且不必引入额外的静态字段


出于某种奇怪的巧合,在我写这篇文章一个月后,一位真正的性能工程师调查了这个问题:参见Alexey Shipilёv的博客文章《古人的智慧阵列》:

正如所料,在非常小的集合大小上可以观察到的唯一效果,这只是比
new Foo[0]
的一个微小改进。这一改进似乎不足以证明将数组缓存在总体方案中是合理的。作为一个微小的微优化,它在一些紧凑的代码中可能是有意义的,但我不在乎其他


这就解决了。我将把记号标出来,献给Alexey。

每次都返回同一个数组不是一个好处,所以每次都不必返回一个数组。就像使用模板一样,VM不能做出这样的假设。例如,您可能想创建一个新的空数组,该数组将在引用相等中使用。这也节省了一些输入:DYou是对的。JVM可能会给编译器带来很多工作。编辑:我们到底为什么需要编译器?它甚至不能合并字符串arr分配!字符串和数组之间的大小写不相等,因为内联“”是字符串常量(与
新字符串(“”
)相比),而没有等效的“数组常量”概念。JLS进一步要求字符串常量必须是intern()才能成为规范的。这是一个非常不公平的基准测试,即使它使用JMH。1) 手动编写基准循环是一个错误;JMH为你做的。2) 结果不会被黑洞吞噬。3) 测试是不可比较的:
testNew
消耗大量内存,因为它将所有中间结果保存在一起;这在实际操作中并不常见。4) 测试的编写方式使得JVM不可能应用分配消除优化,但在处理从方法返回的短期对象时,这种优化是至关重要的(请参阅)。
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();
    }
}
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);
        }
    }
}
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
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