Rangkaian string, atau patch bytecode

Saya baru-baru ini membaca sebuah artikel tentang mengoptimalkan kinerja kode Java - khususnya, penggabungan string. Pertanyaannya tetap ada - mengapa saat menggunakan StringBuilder dalam kode di bawah cut, program berjalan lebih lambat daripada dengan penambahan sederhana. Dalam hal ini, + = saat kompilasi berubah menjadi panggilan StringBuilder.append ().

Saya langsung memiliki keinginan untuk menyelesaikan masalah.

// ~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(); } 

Kemudian semua alasan saya sampai pada kenyataan bahwa ini adalah sihir yang tidak dapat dijelaskan di dalam JVM, dan saya berhenti berusaha untuk menyadari apa yang terjadi. Namun, selama diskusi berikutnya tentang perbedaan antara platform dalam kecepatan bekerja dengan string, teman saya yegorf1 dan saya memutuskan untuk mencari tahu mengapa dan bagaimana tepatnya keajaiban ini terjadi.

Oracle Java SE


upd: tes dilakukan di Java 8
Solusi yang jelas adalah mengumpulkan kode sumber dalam bytecode, dan kemudian melihat isinya. Jadi kami melakukannya. Dalam komentar ada saran bahwa akselerasi terkait dengan optimasi - garis konstan jelas harus direkatkan pada tingkat kompilasi. Ternyata ini tidak benar. Ini adalah bagian dari bytecode yang didekompilasi dengan 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; 

Anda mungkin memperhatikan bahwa tidak ada optimasi yang dilakukan. Aneh bukan? Oke, mari kita lihat bytecode dari fungsi kedua.

  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; 

Di sini lagi, tidak ada optimasi? Selain itu, mari kita lihat instruksi pada 8, 14 dan 15 byte. Suatu hal yang aneh terjadi di sana - pertama, referensi ke objek dari kelas StringBuilder dimuat ke stack, kemudian dilempar dari stack dan dimuat kembali. Solusi paling sederhana muncul di benak:

  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; 

Membuang instruksi yang tidak perlu, kami mendapatkan kode yang bekerja 1,5 kali lebih cepat dari versi stringAppend, di mana optimasi ini telah dilakukan. Dengan demikian, kesalahan "ajaib" adalah kompiler bytecode yang tidak lengkap, yang tidak dapat melakukan optimasi yang cukup sederhana.

ART Android


upd: kode ini dibuat di bawah sdk 28 dengan rilis buildtools
Jadi, ternyata masalahnya terkait dengan implementasi Java compiler dalam bytecode untuk stack JVM. Di sini kami ingat keberadaan ART, yang merupakan bagian dari Proyek Open Source Android . Mesin virtual ini, atau lebih tepatnya, kompiler bytecode ke dalam kode asli, ditulis dalam gugatan dari Oracle, yang memberi kita setiap alasan untuk percaya bahwa perbedaan dari implementasi Oracle adalah signifikan. Selain itu, karena spesifikasi prosesor ARM, mesin virtual ini adalah register, bukan stack.

Mari kita lihat Smali (salah satu representasi bytecode di bawah 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; //... 

Dalam varian stringAppendBuilder ini tidak ada lagi masalah dengan stack - mesin adalah register, dan pada prinsipnya tidak dapat terjadi. Namun, ini tidak mengganggu keberadaan hal-hal yang benar-benar ajaib:

 move-result-object v1 

String ini di stringAppend tidak melakukan apa-apa - tautan ke objek StringBuilder yang kita butuhkan sudah ada dalam register v1. Adalah logis untuk mengasumsikan bahwa stringAppend akan bekerja lebih lambat. Ini dikonfirmasi secara eksperimental - hasilnya mirip dengan hasil versi "patched" dari program untuk stack JVM: StringBuilder bekerja hampir satu setengah kali lebih cepat.

Source: https://habr.com/ru/post/id416479/


All Articles