最近,我读
了一篇有关优化Java代码性能
的文章 -特别是字符串连接。 问题仍然存在-为什么在削减的代码中使用StringBuilder时,该程序的运行速度比简单添加时要慢。 在这种情况下,+ =在编译期间会转换为StringBuilder.append()调用。
我立即有一个希望解决问题的愿望。
然后,我所有的推理都归结为事实,这是JVM内部无法解释的魔力,我放弃了尝试去了解发生了什么。 但是,在下一次讨论平台之间在处理字符串的速度方面的差异时,我的朋友
yegorf1和我决定弄清楚这种魔术的产生原因和发生方式。
Oracle Java SE
upd:测试是在Java 8上进行的显而易见的解决方案是收集字节码形式的源代码,然后查看其内容。 我们做到了。 在评论中,
有人建议加速与优化有关-常量行显然应该在编译级别粘合在一起。 事实证明并非如此。 这是用javap反编译的字节码的一部分:
public java.lang.String stringAppend(); Code: 0: ldc #2
您可能会注意到没有进行任何优化。 奇怪,不是吗? 好的,让我们看看第二个函数的字节码。
public java.lang.String stringAppendBuilder(); Code: 0: new #3
同样,这里没有优化吗? 此外,让我们看一下有关8、14和15字节的指令。 在那里发生了一件奇怪的事情-首先,将对StringBuilder类的对象的引用加载到堆栈上,然后将其从堆栈中抛出并重新加载。 我想到了最简单的解决方案:
public java.lang.String stringAppendBuilder(); Code: 0: new #41
剔除不必要的指令,我们得到的代码的运行速度比stringAppend版本快1.5倍,在该版本中已经进行了优化。 因此,“魔术师”的错误是字节码编译器不完整,无法执行非常简单的优化。
Android ART
upd:该代码是使用发布buildtools在sdk 28下构建的因此,事实证明,该问题与堆栈JVM的字节码中Java编译器的实现有关。 在这里,我们记得
ART的存在
,它是Android Open Source Project的一部分 。 这个虚拟机,或者说是将字节码编译器转换为本地代码,是在Oracle的一次
诉讼中编写的,这使我们有充分的理由相信与Oracle实现的差异是巨大的。 另外,由于ARM处理器的特性,该虚拟机是寄存器,而不是堆栈。
让我们看一下Smali(ART下的字节码表示形式之一):
# virtual methods .method public stringAppend()Ljava/lang/String; .registers 4 .prologue .line 6 const-string/jumbo v0, "foo" .line 7 .local v0, "s":Ljava/lang/String; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 const-string/jumbo v2, ", bar" invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1
在这个stringAppendBuilder变体中,堆栈不再有问题-机器是寄存器,原则上不会出现问题。 但是,这不会干扰绝对神奇的事物的存在:
move-result-object v1
stringAppend中的此字符串不执行任何操作-所需的StringBuilder对象的链接已在v1寄存器中。 假设stringAppend的运行速度会更慢是合乎逻辑的。 这已通过实验得到证实-结果类似于堆栈JVM程序的“修补”版本的结果:StringBuilder的运行速度快了近一半半。