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

短篇故事

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

图1

如果你的应用程序的性能与比较数组的相等性非常相关,那么这意味着你非常希望将所有数据强制转换为char[] ,只是为了获得这种神奇的性能提升。

很长的故事

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

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

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

现在,~8x变种的这种优势并没有扩大到更小或更大的arrays – 但它仍然更快。

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

图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[]或double以避免使图形混乱,但是对于记录boolean[]float[]执行与int[]相同,而double[]执行与long[]相同的操作。 根据类型的基础大小,这是有道理的。

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

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

以下JMH基准测试certificate,禁用此内在函数会使char[] short[]数组比较与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 

这种内在很久以前是在2008年积极的JVM竞争时期加入的。 JDK 6包含一个特殊的alt-string.jar库,它由-XX:+UseStringCache启用。 我从其中一个特殊类 – StringValue.StringCache找到了一些对Arrays.equals(char[], char[])的调用。 内在性是这种“优化”的重要组成部分。 在现代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 

在JDK 9中, Arrays.equals实现已经改变。

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

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

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

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

在Scala和GoSu中也可以观察到相同的行为。 这些天传输的大部分信息都是文本forms,因此,除非您修改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才存在。