基于我对几个不同的Oracle和OpenJDK实现的测试,Arrays.equals(char[], char[])
似乎比其他类型的所有其他变体的快8倍。
如果您的应用程序的性能与 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[]
数组在大尺寸下的性能开始下降。这里看一下相同的测试,但是有一个较小的小阵列和较大的大阵列:
在这里,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 我在这里撒谎。表现可能突然发生了变化但我没有注意到,直到我再次运行基准测试,经过一系列其他变化后,导致一个痛苦的二分过程,我确定了因果关系的变化。这是进行某种性能测量持续集成的一个很好的理由。
答案 0 :(得分:25)
@ Marco13猜对了。 HotSpot JVM对Arrays.equals
有an 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#l756,Arrays#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://www.tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-7.html
说真的,SSE4.x不会将字符和字节视为等效数据类型,这就是文本分析速度更快的原因。此外,对于8位积分,比较指令直到AVX2才存在。