为什么Arrays.equals(char [],char [])比其他所有版本快8倍?

Bee*_*ope 24 java arrays optimization performance

短篇故事

根据我与几个不同的Oracle和OpenJDK的实现测试,似乎Arrays.equals(char[], char[])是某种大约快8倍比其他类型的所有其他变体.

图1

如果您的应用程序的性能在很大程度上相关0与平等比较阵列,这意味着你很可能要强制所有的数据到char[],只是为了得到这个神奇的性能提升.

很长的故事

最近我写了一些高性能代码,用于Arrays.equals(...)比较用于索引到结构的键.密钥可能很长,并且通常仅在后面的字节中有所不同,因此这种方法的性能非常重要.

在一个点我用类型的键char[],但作为推广服务的一部分,并且避免从底层的来源一些副本byte[]ByteBuffer,我改变这对byte[].突然2,许多基本操作的表现下降了约3倍.我将其追溯到上述事实:Arrays.equals(char[], char[])似乎在所有其他Arrays.equals()版本中享有特殊状态,包括在short[]语义上相同的一个版本(并且可以使用相同的底层代码实现,因为签名不会影响equals的行为).

所以我写了一个JMH基准测试的所有原始变种Arrays.equals(...)1char[]变异击碎所有其它的,如上图所示.

现在,~8x变种的这种优势并没有扩大到更小或更大的阵列 - 但它仍然更快.

对于小型阵列,似乎常数因素开始占主导地位,对于较大的阵列,L2/L3或主内存带宽开始起作用(您可以在前面的图中清楚地看到后者的影响,其中int[]尤其是long[]阵列开始降低大尺寸的性能).下面是相同的测试,但是有一个较小的小数组和较大的大数组:

图2

在这里,char[]仍然踢屁股,只是没有像以前那么多.小数组(仅16个元素)的每个元素时间大约是标准时间的两倍,可能是由于函数开销:在大约0.5 ns /元素时,char[]整个调用的变量仍然只需要大约7.2纳秒,或大约19纳秒在我的机器上循环 - 所以少量的方法开销会大量削减运行时间(同样,基准开销本身就是几个循环).

在大端,缓存和/或内存带宽是一个驱动因素 - long[]变体几乎是变体的2 倍int[].的short[],尤其是byte[]变种都不是很有效的(他们的工作设置仍然适用于L3在我的机器).

char[]与其他所有应用程序之间的差异非常大,以至于对于依赖于阵列比较的应用程序(对于某些特定域而言,这实际上并不常见),尝试将所有数据char[]用于利用是值得的.呵呵.

是什么赋予了?是否char得到特殊待遇,因为它是某些String方法的基础?它只是JVM优化方法的另一种情况,它在基准测试中受到很大影响,而不是将相同(明显)的优化扩展到其他原始类型(特别short是在这里相同)?


0 ...并且这甚至都不是那么疯狂 - 考虑各种系统,例如,依赖于(冗长的)散列比较以检查值是否相等,或者哈希映射,其中键是长的或可变大小的.

1我不包括 boolean[],float[]并且double[]或者在结果双以避免混乱的图表,但对于记录boolean[]float[]执行相同int[],而double[]所执行的相同long[].根据类型的基础大小,这是有道理的.

2我躺在这里了一下.表现可能会突然发生变化,但在我进行了一系列其他变化之后,我再次运行基准测试后才真正注意到,导致一个痛苦的二分过程,我确定了因果关系的变化.这是进行某种性能测量持续集成的一个很好的理由.

apa*_*gin 25

@ Marco13猜对了.HotSpot JVM有一个内在的(即特殊的手工编码实现)Arrays.equals(char[], char[]),但不适用于其他Arrays.equals方法.

以下JMH基准测试证明,禁用此内在函数会使数组比较char[]short[]数组比较一样慢.

@State(Scope.Benchmark)
public class ArrayEquals {
    @Param("100")
    int length;

    short[] s1, s2;
    char[] c1, c2;

    @Setup
    public void setup() {
        s1 = new short[length];
        s2 = new short[length];
        c1 = new char[length];
        c2 = new char[length];
    }

    @Benchmark
    public boolean chars() {
        return Arrays.equals(c1, c2);
    }

    @Benchmark
    @Fork(jvmArgsAppend = {"-XX:+UnlockDiagnosticVMOptions", "-XX:DisableIntrinsic=_equalsC"})
    public boolean charsNoIntrinsic() {
        return Arrays.equals(c1, c2);
    }

    @Benchmark
    public boolean shorts() {
        return Arrays.equals(s1, s2);
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

Benchmark                     (length)  Mode  Cnt   Score   Error  Units
ArrayEquals.chars                  100  avgt   10  19,012 ± 1,204  ns/op
ArrayEquals.charsNoIntrinsic       100  avgt   10  49,495 ± 0,682  ns/op
ArrayEquals.shorts                 100  avgt   10  49,566 ± 0,815  ns/op
Run Code Online (Sandbox Code Playgroud)

这种内在的加入前不久在2008年积极的JVM竞争的时代.JDK 6包含一个特殊的alt-string.jar库,该库由启用-XX:+UseStringCache.我Arrays.equals(char[], char[])从其中一个特殊课程中找到了许多电话- StringValue.StringCache.内在性是这种"优化"的重要组成部分.在现代JDK中已经没有了alt-string.jar,但JVM内在仍然存在(尽管没有发挥其原始作用).

更新

我用JDK 9-ea + 148进行了相同的测试,看起来_equalsC内在性能差别很小.

Benchmark                     (length)  Mode  Cnt   Score   Error  Units
ArrayEquals.chars                  100  avgt   10  18,931 ± 0,061  ns/op
ArrayEquals.charsNoIntrinsic       100  avgt   10  19,616 ± 0,063  ns/op
ArrayEquals.shorts                 100  avgt   10  19,753 ± 0,080  ns/op
Run Code Online (Sandbox Code Playgroud)

Arrays.equals JDK 9中的实现已经改变.

现在它ArraysSupport.vectorizedMismatch为所有类型的非对象数组调用helper方法.此外,vectorizedMismatch它也是一个HotSpot内在函数,它具有使用AVX 的手写程序集实现.

  • @ Marco13这是[代码](http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/812ed44725b8/src/cpu/x86/vm/macroAssembler_x86.cpp#l6844) (3认同)
  • @ Marco13 BTW,我刚刚测试了JDK 9-EA + 148,并且Arrays.compare的内在版本和非内在版本之间几乎没有差异. (3认同)
  • @BeeOnRope实际上,JDK 9具有用于快速数组比较的新内在函数.我已经更新了答案. (3认同)

Mar*_*o13 9

在建议这是答案时,我可能会出局,但根据http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/9d15b81d5d1b/src/share/vm/classfile/vmSymbols. hpp#l756,该Arrays#equals(char[], char[])方法实现为内在的.

最有可能的原因是它在所有字符串比较中都具有高性能关键性.< - 这至少是错的.令人惊讶的是,String不Arrays.equals用于比较.但不管它为什么是内在的,这可能仍然是性能差异的原因.

  • 不可否认,直到现在,这个"答案"还是一个"猜测".人们可以使用"PrintCompilation","PrintInlining"和"PrintAssembly"来更彻底地验证它,甚至可以尝试*禁用*这个内在函数以确保它确实*是*更高性能的原因.我希望有人分配一些时间来创建一个比这更明确(和可接受)的答案. (2认同)
  • 好的,我已经添加了支持您最初猜测的答案.与此同时,我发现了这种内在的最初目的. (2认同)