字符串串联或补丁字节码

最近,我读了一篇有关优化Java代码性能的文章 -特别是字符串连接。 问题仍然存在-为什么在削减的代码中使用StringBuilder时,该程序的运行速度比简单添加时要慢。 在这种情况下,+ =在编译期间会转换为StringBuilder.append()调用。

我立即有一个希望解决问题的愿望。

// ~20 000 000    public String stringAppend() { String s = "foo"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; return s; } // ~7 000 000    public String stringAppendBuilder() { StringBuilder sb = new StringBuilder(); sb.append("foo"); sb.append(", bar"); sb.append(", bar"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); return sb.toString(); } 

然后,我所有的推理都归结为事实,这是JVM内部无法解释的魔力,我放弃了尝试去了解发生了什么。 但是,在下一次讨论平台之间在处理字符串的速度方面的差异时,我的朋友yegorf1和我决定弄清楚这种魔术的产生原因和发生方式。

Oracle Java SE


upd:测试是在Java 8上进行的
显而易见的解决方案是收集字节码形式的源代码,然后查看其内容。 我们做到了。 在评论中, 有人建议加速与优化有关-常量行显然应该在编译级别粘合在一起。 事实证明并非如此。 这是用javap反编译的字节码的一部分:

  public java.lang.String stringAppend(); Code: 0: ldc #2 // String foo 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: aload_1 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

您可能会注意到没有进行任何优化。 奇怪,不是吗? 好的,让我们看看第二个函数的字节码。

  public java.lang.String stringAppendBuilder(); Code: 0: new #3 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: pop 15: aload_1 16: ldc #6 // String , bar 18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

同样,这里没有优化吗? 此外,让我们看一下有关8、14和15字节的指令。 在那里发生了一件奇怪的事情-首先,将对StringBuilder类的对象的引用加载到堆栈上,然后将其从堆栈中抛出并重新加载。 我想到了最简单的解决方案:

  public java.lang.String stringAppendBuilder(); Code: 0: new #41 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

剔除不必要的指令,我们得到的代码的运行速度比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 //... .method public stringAppendBuilder()Ljava/lang/String; .registers 3 .prologue .line 13 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V .line 14 .local v0, "sb":Ljava/lang/StringBuilder; const-string/jumbo v1, "foo" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; .line 15 const-string/jumbo v1, ", bar" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; //... 

在这个stringAppendBuilder变体中,堆栈不再有问题-机器是寄存器,原则上不会出现问题。 但是,这不会干扰绝对神奇的事物的存在:

 move-result-object v1 

stringAppend中的此字符串不执行任何操作-所需的StringBuilder对象的链接已在v1寄存器中。 假设stringAppend的运行速度会更慢是合乎逻辑的。 这已通过实验得到证实-结果类似于堆栈JVM程序的“修补”版本的结果:StringBuilder的运行速度快了近一半半。

Source: https://habr.com/ru/post/zh-CN416479/


All Articles