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

时间:2016-12-14 23:27:35

标签: java arrays performance optimization

短篇小说

基于我对几个不同的Oracle和OpenJDK实现的测试,Arrays.equals(char[], char[])似乎比其他类型的所有其他变体的快8倍

Figure 1

如果您的应用程序的性能与 0 相关,并且比较数组是否相等,这意味着您非常希望将所有数据强制转换为char[],只是为了得到这个魔术性能提升。

长篇故事

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

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

所以我写了一个JMH benchmark来测试Arrays.equals(...) 1 的所有原始变体,char[]变体压缩所有其他变体,如上所示。

现在,~8x品种的这种优势并没有以相同的幅度扩展到更小或更大的阵列 - 但它仍然更快。

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

Figure 2

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

在大端,缓存和/或内存带宽是一个驱动因素 - long[]变体几乎是int[]变体的2倍。 short[],尤其是byte[]变体并非常有效(他们的工作集仍然适合我机器中的L3)。

char[]和所有其他版本之间的差异非常大,以至于对于依赖于数组比较的应用程序(这对于某些特定域而言实际上并不常见),它是值得的尝试将所有数据都放入char[]以便利用。咦。

是什么给出的? char是否得到特殊处理,因为它是某些String方法的基础?它只是JVM优化方法的另一个案例,它在基准测试中受到很大影响,而不是将相同(明显)的优化扩展到其他基本类型(尤其是short 相同这里) ?

0 ......并且这些都不是那么疯狂 - 考虑各种系统,例如,依赖于(冗长的)哈希比较以检查值是否相等,或者哈希密钥长或大小不同的地图。

1 我没有在结果中包含boolean[]float[]double[]或加倍,以避免混乱图表,但是为了记录boolean[]float[]执行与int[]相同,而double[]执行与long[]相同。根据类型的基础大小,这是有道理的。

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

3 个答案:

答案 0 :(得分:25)

@ Marco13猜对了。 HotSpot JVM对Arrays.equalsan intrinsic(即特殊的手工编码实现),但对其他char[]方法则没有。

以下JMH基准测试证明,禁用此内在函数会使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); } } 数组比较一样慢。

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

结果:

alt-string.jar

这个内在的was added很久以前就是在2008年积极的JVM竞争时代。 JDK 6包含一个由-XX:+UseStringCache启用的特殊Arrays.equals(char[], char[])库。我从其中一个特殊类别StringValue.StringCache找到了alt-string.jar的一些电话。内在是这个"优化"的重要组成部分。在现代JDK中,不再有_equalsC,但JVM内在仍然存在(尽管没有发挥其原始作用)。

<强>更新

我已经用JDK 9-ea + 148对它进行了相同的测试,看起来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 内在的性能差异非常小。

Arrays.equals
JDK 9中的

vectorizedMismatch实现已发生变化。

现在它为所有类型的非对象数组调用ArraysSupport.vectorizedMismatch辅助方法。此外,{{1}}也是一个使用AVX进行hand-written assembly实现的HotSpot内在函数。

答案 1 :(得分:9)

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

最有可能的原因是它在所有字符串比较中都具有很高的性能。&lt; - 这至少是错误的。令人惊讶的是,String不使用Arrays.equals进行比较。但无论为什么它都是内在的,这可能仍然是性能差异的原因。

答案 2 :(得分:-4)

因为对于字符,SSE3和4.1 / 4.2都非常擅长检查状态变化。 JVM生成的char操作代码代码更加优化,因为这是Java在Web应用程序等中经常使用的内容。 Java在优化其他类型的数据方面非常糟糕。这就是野兽的本性。

在Scala和GoSu中也可以观察到相同的行为。这些天传输的大部分信息都是文本形式,因此除非您修改JVM,否则它会针对文本进行调整。并且,正如Marco提到的,它是一个内在的C函数,意味着它直接映射到高性能矢量化指令,如SSE4.x甚至AVX2,如果标准JVM得到了很大的改进。

http://blog.synopse.info/post/2015/06/30/Faster-String-process-using-SSE-4.2-Text-Processing-Instructions-STTNI

http://www.tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-7.html

说真的,SSE4.x不会将字符和字节视为等效数据类型,这就是文本分析速度更快的原因。此外,对于8位积分,比较指令直到AVX2才存在。