Haruskah saya menggunakan String.format () Java jika kinerjanya penting?

215

Kita harus membuat Strings setiap saat untuk keluaran log dan sebagainya. Lebih dari versi JDK kami telah belajar kapan harus menggunakan StringBuffer(banyak menambahkan, aman thread) dan StringBuilder(banyak menambahkan, aman non-thread).

Apa saran untuk menggunakan String.format()? Apakah efisien, atau apakah kita dipaksa untuk tetap dengan penggabungan satu baris di mana kinerja itu penting?

misalnya gaya lama yang jelek,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

vs. merapikan gaya baru (String.format, yang mungkin lebih lambat),

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

Catatan: kasus penggunaan spesifik saya adalah ratusan string log 'satu-liner' di seluruh kode saya. Mereka tidak melibatkan lingkaran, jadi StringBuilderterlalu berat. Saya tertarik secara String.format()khusus.

Udara
sumber
28
Mengapa Anda tidak mengujinya?
Ed S.
1
Jika Anda menghasilkan output ini, maka saya menganggap itu harus dapat dibaca oleh manusia sebagai tingkat yang dapat dibaca manusia. Katakan 10 baris per detik paling banyak. Saya pikir Anda akan menemukan itu benar-benar tidak masalah pendekatan mana yang Anda ambil, jika secara perlahan dikatakan lambat, pengguna mungkin akan menghargainya. ;) Jadi tidak, StringBuilder tidak kelas berat di sebagian besar situasi.
Peter Lawrey
9
@ Peter, tidak, itu sama sekali tidak untuk dibaca secara real time oleh manusia! Itu ada untuk membantu analisis ketika ada masalah. Output log biasanya ribuan baris per detik, sehingga perlu efisien.
Air
5
jika Anda menghasilkan ribuan baris per detik, saya akan menyarankan 1) menggunakan teks lebih pendek, bahkan tidak ada teks seperti CSV biasa, atau biner 2) Jangan gunakan String sama sekali, Anda dapat menulis data ke dalam ByteBuffer tanpa membuat objek apa pun (sebagai teks atau biner) 3) melatarbelakangi penulisan data ke disk atau soket. Anda harus dapat mempertahankan sekitar 1 juta baris per detik. (Pada dasarnya sebanyak yang diizinkan oleh subsistem disk Anda) Anda dapat mencapai semburan 10x ini.
Peter Lawrey
7
Ini tidak relevan dengan kasus umum, tetapi untuk logging pada khususnya, LogBack (ditulis oleh penulis Log4j asli) memiliki bentuk logging parameter yang mengatasi masalah ini - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell

Jawaban:

122

Saya menulis sebuah kelas kecil untuk menguji yang memiliki kinerja lebih baik dari keduanya dan + datang di depan format. dengan faktor 5 hingga 6. Cobalah sendiri

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

Menjalankan hal di atas untuk N yang berbeda menunjukkan bahwa keduanya berperilaku linier, tetapi String.format5-30 kali lebih lambat.

Alasannya adalah bahwa dalam implementasi saat ini String.formatpertama mem-parsing input dengan ekspresi reguler dan kemudian mengisi parameter. Penggabungan dengan plus, di sisi lain, akan dioptimalkan oleh javac (bukan oleh JIT) dan digunakan StringBuilder.appendsecara langsung.

Perbandingan runtime

hhafez
sumber
12
Ada satu kelemahan dengan tes ini karena itu tidak sepenuhnya merupakan representasi yang baik dari semua pemformatan string. Seringkali ada logika yang terlibat dalam apa yang harus dimasukkan dan logika untuk memformat nilai-nilai spesifik menjadi string. Setiap tes nyata harus melihat skenario dunia nyata.
Orion Adrian
9
Ada pertanyaan lain pada SO tentang + ayat StringBuffer, dalam versi terbaru Java + diganti dengan StringBuffer jika memungkinkan sehingga kinerjanya tidak akan berbeda
hhafez
25
Ini sangat mirip dengan microbenchmark yang akan dioptimalkan dengan cara yang sangat tidak berguna.
David H. Clements
20
Mikro-benchmark lain yang tidak diterapkan dengan baik. Bagaimana kedua metode skala dengan urutan besarnya. Bagaimana dengan menggunakan operasi, 100, 1000, 10000, 1000000. Jika Anda hanya menjalankan satu tes, pada satu urutan besarnya, pada aplikasi yang tidak berjalan pada inti yang terisolasi; tidak ada cara untuk mengatakan berapa banyak perbedaan dapat dihapuskan sebagai 'efek samping' karena pengalihan konteks, proses latar belakang, dll.
Evan Plaice
8
Selain itu karena Anda tidak pernah keluar dari JIT utama tidak bisa masuk.
Jan Zyka
241

Saya mengambil kode hhafez dan menambahkan tes memori :

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

Saya menjalankan ini secara terpisah untuk setiap pendekatan, operator '+', String.format dan StringBuilder (memanggil toString ()), sehingga memori yang digunakan tidak akan terpengaruh oleh pendekatan lain. Saya menambahkan lebih banyak rangkaian, menjadikan string sebagai "Blah" + i + "Blah" + i + "Blah" + i + "Blah".

Hasilnya adalah sebagai berikut (rata-rata 5 berjalan masing-masing):
Waktu Pendekatan (ms) Memori yang dialokasikan (panjang)
operator '+' 747 320.504
String.format 16484 373.312
StringBuilder 769 57.344

Kita dapat melihat bahwa String '+' dan StringBuilder secara praktis identik dengan waktu, tetapi StringBuilder jauh lebih efisien dalam penggunaan memori. Ini sangat penting ketika kami memiliki banyak panggilan log (atau pernyataan lain yang melibatkan string) dalam interval waktu yang cukup singkat sehingga Pengumpul Sampah tidak akan bisa membersihkan banyak instance string yang dihasilkan oleh operator '+'.

Dan sebuah catatan, BTW, jangan lupa untuk memeriksa level logging sebelum membuat pesan.

Kesimpulan:

  1. Saya akan terus menggunakan StringBuilder.
  2. Saya memiliki terlalu banyak waktu atau terlalu sedikit kehidupan.
Itamar
sumber
8
"jangan lupa untuk memeriksa tingkat pencatatan sebelum menyusun pesan", adalah saran yang bagus, ini harus dilakukan setidaknya untuk pesan debug, karena mungkin ada banyak dan mereka tidak boleh diaktifkan dalam produksi.
stivlo
39
Tidak, ini tidak benar. Maaf untuk menjadi tumpul tetapi jumlah upvotes telah menarik tidak kekurangan mengkhawatirkan. Menggunakan +operator mengkompilasi ke StringBuilderkode yang setara . Microbenchmark seperti ini bukan cara yang baik untuk mengukur kinerja - mengapa tidak menggunakan jvisualvm, ada di jdk karena suatu alasan. String.format() akan lebih lambat, tetapi karena waktu untuk mem-parsing string format daripada alokasi objek apa pun. Menunda pembuatan artefak penebangan hingga Anda yakin mereka membutuhkannya adalah saran yang bagus, tetapi jika itu akan memiliki dampak kinerja itu di tempat yang salah.
CurtainDog
1
@CurtainDog, komentar Anda dibuat pada pos berusia empat tahun, dapatkah Anda menunjuk ke dokumentasi atau membuat jawaban terpisah untuk mengatasi perbedaannya?
kurtzbot
1
Referensi yang mendukung komentar @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Yaitu, + lebih disukai kecuali dilakukan dalam satu lingkaran.
aprikot
And a note, BTW, don't forget to check the logging level before constructing the message.bukan saran yang bagus. Dengan asumsi kita sedang membicarakan java.util.logging.*secara spesifik, memeriksa level logging adalah ketika Anda berbicara tentang melakukan pemrosesan lanjutan yang akan menyebabkan efek buruk pada suatu program yang tidak Anda inginkan ketika sebuah program tidak memiliki logging yang diaktifkan ke tingkat yang sesuai. Pemformatan string bukan tipe pemrosesan sama sekali. Pemformatan adalah bagian dari java.util.loggingframework, dan logger itu sendiri memeriksa level logging sebelum pemformat dipanggil.
searchengine27
30

Semua tolok ukur yang disajikan di sini memiliki beberapa kekurangan , sehingga hasilnya tidak dapat diandalkan.

Saya terkejut bahwa tidak ada yang menggunakan JMH untuk benchmark, jadi saya melakukannya.

Hasil:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

Unit adalah operasi per detik, semakin banyak semakin baik. Kode sumber patokan . OpenJDK IcedTea 2.5.4 Java Virtual Machine digunakan.

Jadi, gaya lama (menggunakan +) jauh lebih cepat.

Adam Stelmaszczyk
sumber
5
Ini akan jauh lebih mudah untuk ditafsirkan jika Anda menjelaskan yang "+" dan yang "format".
AjahnCharles
21

Gaya jelek lama Anda secara otomatis dikompilasi oleh JAVAC 1.6 sebagai:

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

Jadi sama sekali tidak ada perbedaan antara ini dan menggunakan StringBuilder.

String.format jauh lebih berat karena ia menciptakan Formatter baru, mem-parsing string format input Anda, membuat StringBuilder, menambahkan segalanya ke sana dan memanggil toString ().

Raphaël
sumber
Dalam hal keterbacaan, kode yang Anda posting jauh lebih ... rumit daripada String.format ("Apa yang Anda dapatkan jika Anda mengalikan% d dengan% d?", VarSix, varNine);
dusktreader
12
Tidak ada perbedaan antara +dan StringBuildermemang. Sayangnya ada banyak informasi yang salah dalam jawaban lain di utas ini. Saya hampir tergoda untuk mengubah pertanyaan how should I not be measuring performance.
CurtainDog
12

String.format Java berfungsi seperti ini:

  1. itu mem-parsing string format, meledak ke dalam daftar potongan format
  2. itu memotong potongan format, rendering ke dalam StringBuilder, yang pada dasarnya adalah sebuah array yang mengubah ukurannya sendiri sesuai kebutuhan, dengan menyalin ke dalam array baru. ini perlu karena kita belum tahu seberapa besar untuk mengalokasikan String terakhir
  3. StringBuilder.toString () menyalin buffer internal ke String baru

jika tujuan akhir untuk data ini adalah streaming (misalnya, merender halaman web atau menulis ke file), Anda dapat merakit potongan format langsung ke aliran Anda:

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

Saya berspekulasi bahwa optimizer akan mengoptimalkan pemrosesan string format. Jika demikian, Anda memiliki kinerja amortisasi yang setara untuk membuka gulungan String.format secara manual ke dalam StringBuilder.

Dustin Getz
sumber
5
Saya tidak berpikir spekulasi Anda tentang optimasi pemrosesan string format sudah benar. Dalam beberapa tes dunia nyata menggunakan Java 7, saya menemukan bahwa menggunakan String.formatloop dalam (berjalan jutaan kali) menghasilkan lebih dari 10% dari waktu eksekusi saya dihabiskan di java.util.Formatter.parse(String). Ini tampaknya menunjukkan bahwa dalam loop internal, Anda harus menghindari panggilan Formatter.formatatau apa pun yang menyebutnya, termasuk PrintStream.format(cacat dalam lib standar Java, IMO, terutama karena Anda tidak dapat men-cache string format yang diuraikan).
Andy MacKinlay
8

Untuk memperluas / memperbaiki jawaban pertama di atas, sebenarnya bukan terjemahan yang akan membantu String.format.
Apa String.format akan membantu adalah ketika Anda mencetak tanggal / waktu (atau format numerik, dll), di mana ada perbedaan lokalisasi (l10n) (yaitu, beberapa negara akan mencetak 04Feb2009 dan yang lain akan mencetak Feb042009).
Dengan terjemahan, Anda hanya berbicara tentang memindahkan string eksternal (seperti pesan kesalahan dan apa-tidak) ke dalam bundel properti sehingga Anda dapat menggunakan bundel yang tepat untuk bahasa yang tepat, menggunakan ResourceBundle dan MessageFormat.

Melihat semua hal di atas, saya akan mengatakan bahwa concatenation, String.format vs plain datang ke apa yang Anda inginkan. Jika Anda lebih suka melihat panggilan ke .format daripada gabungan, maka tentu saja, ikuti saja.
Bagaimanapun, kode lebih banyak dibaca daripada yang tertulis.

dw.mackie
sumber
1
Saya akan mengatakan bahwa Rangkaian Kinerja, String.format vs polos datang ke apa yang Anda suka Saya pikir ini salah. Dari segi kinerja, penggabungan jauh lebih baik. Untuk lebih jelasnya silakan lihat jawaban saya.
Adam Stelmaszczyk
6

Dalam contoh Anda, masalah kinerja tidak terlalu berbeda tetapi ada masalah lain yang perlu dipertimbangkan: yaitu fragmentasi memori. Bahkan operasi gabungan membuat string baru, bahkan jika itu sementara (butuh waktu untuk membuatnya dan itu lebih banyak bekerja). String.format () lebih mudah dibaca dan melibatkan lebih sedikit fragmentasi.

Juga, jika Anda sering menggunakan format tertentu, jangan lupa Anda dapat menggunakan kelas Formatter () secara langsung (semua String.format () yang digunakan adalah instantiate turunan satu kali pakai Formatter).

Selain itu, hal lain yang harus Anda perhatikan: berhati-hatilah menggunakan substring (). Sebagai contoh:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

String besar itu masih ada dalam memori karena itulah cara kerja substring Java. Versi yang lebih baik adalah:

  return new String(largeString.substring(100, 300));

atau

  return String.format("%s", largeString.substring(100, 300));

Bentuk kedua mungkin lebih berguna jika Anda melakukan hal-hal lain pada saat yang bersamaan.

cletus
sumber
8
Layak menunjukkan "pertanyaan terkait" sebenarnya C # dan karenanya tidak berlaku.
Air
alat apa yang Anda gunakan untuk mengukur fragmentasi memori dan apakah fragmentasi bahkan membuat perbedaan kecepatan untuk ram?
kritzikratzi
Perlu ditunjukkan bahwa metode substring telah diubah dari Java 7+. Sekarang harus mengembalikan representasi String baru yang hanya berisi karakter substringed. Itu berarti bahwa tidak perlu mengembalikan panggilan String :: new
João Rebelo
5

Secara umum Anda harus menggunakan String.Format karena relatif cepat dan mendukung globalisasi (dengan asumsi Anda benar-benar mencoba menulis sesuatu yang dibaca oleh pengguna). Ini juga memudahkan untuk mengglobal jika Anda mencoba menerjemahkan satu string lawan 3 atau lebih per pernyataan (terutama untuk bahasa yang memiliki struktur tata bahasa yang sangat berbeda).

Sekarang jika Anda tidak pernah berencana untuk menerjemahkan apa pun, maka andalkan mengandalkan konversi + operator bawaan Java StringBuilder. Atau gunakan Java StringBuildersecara eksplisit.

Orion Adrian
sumber
3

Perspektif lain dari sudut pandang Hanya logging.

Saya melihat banyak diskusi terkait dengan masuk pada utas ini sehingga saya berpikir untuk menambahkan pengalaman saya sebagai jawaban. Mungkin seseorang akan merasakan manfaatnya.

Saya kira motivasi logging menggunakan formatter berasal dari menghindari rangkaian string. Pada dasarnya, Anda tidak ingin memiliki overhead string concat jika Anda tidak akan mencatatnya.

Anda tidak perlu melakukan concat / format kecuali Anda ingin login. Katakanlah jika saya mendefinisikan metode seperti ini

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

Dalam pendekatan ini pembatalan / formatter tidak benar-benar dipanggil sama sekali jika itu pesan debug dan debugOn = false

Meskipun masih akan lebih baik menggunakan StringBuilder daripada formatter di sini. Motivasi utama adalah untuk menghindari semua itu.

Pada saat yang sama saya tidak suka menambahkan blok "jika" untuk setiap pernyataan logging sejak itu

  • Itu mempengaruhi keterbacaan
  • Mengurangi cakupan pada pengujian unit saya - itu membingungkan ketika Anda ingin memastikan setiap baris diuji.

Oleh karena itu saya lebih suka membuat kelas utilitas pencatatan dengan metode seperti di atas dan menggunakannya di mana-mana tanpa khawatir tentang kinerja dan masalah lain yang terkait dengannya.

software.wikipedia
sumber
Bisakah Anda memanfaatkan perpustakaan yang ada seperti slf4j-api yang dimaksudkan untuk mengatasi penggunaan ini dengan fitur pencatatan parameter? slf4j.org/faq.html#logging_performance
ammianus
2

Saya baru saja memodifikasi tes hhafez untuk memasukkan StringBuilder. StringBuilder adalah 33 kali lebih cepat dari String.format menggunakan klien jdk 1.6.0_10 pada XP. Menggunakan -server switch menurunkan faktor menjadi 20.

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

Meskipun ini mungkin terdengar drastis, saya menganggapnya hanya relevan dalam kasus yang jarang terjadi, karena angka absolutnya cukup rendah: 4d untuk 1 juta panggilan String.format sederhana agak ok - selama saya menggunakannya untuk logging atau Suka.

Pembaruan: Seperti yang ditunjukkan oleh sjbotha dalam komentar, tes StringBuilder tidak valid, karena tidak ada final .toString().

Faktor percepatan yang benar dari String.format(.)ke StringBuilderadalah 23 pada mesin saya (16 dengan -serversakelar).

the.duckman
sumber
1
Tes Anda tidak valid karena gagal memperhitungkan waktu yang dihabiskan hanya dengan satu putaran. Anda harus memasukkan itu dan kurangi dari semua hasil lainnya, minimal (ya itu bisa menjadi persentase yang signifikan).
cletus
Saya melakukan itu, for loop membutuhkan 0 ms. Tetapi bahkan jika itu butuh waktu, ini hanya akan meningkatkan faktor.
the.duckman
3
Tes StringBuilder tidak valid karena tidak memanggil toString () pada akhirnya untuk memberi Anda sebuah String yang dapat Anda gunakan. Saya menambahkan ini dan hasilnya adalah bahwa StringBuilder membutuhkan waktu yang sama dengan +. Saya yakin ketika Anda meningkatkan jumlah penambahan pada akhirnya akan menjadi lebih murah.
Sarel Botha
1

Ini adalah versi modifikasi dari entri hhafez. Ini termasuk opsi pembuat string.

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

Waktu setelah untuk loop 391 Waktu setelah untuk loop 4163 Waktu setelah untuk loop 227

SEGERA
sumber
0

Jawabannya sangat tergantung pada bagaimana kompiler Java spesifik Anda mengoptimalkan bytecode yang dihasilkannya. String tidak dapat diubah dan, secara teoritis, setiap operasi "+" dapat membuat yang baru. Tapi, kompiler Anda hampir pasti mengoptimalkan langkah-langkah sementara dalam membangun string panjang. Sangat mungkin bahwa kedua baris kode di atas menghasilkan bytecode yang sama persis.

Satu-satunya cara nyata untuk mengetahuinya adalah dengan menguji kode iteratif di lingkungan Anda saat ini. Tulis aplikasi QD yang menggabungkan string dua arah secara iteratif dan lihat bagaimana mereka saling berhadapan.

Ya - Jake itu.
sumber
1
Bytecode untuk contoh kedua pasti memanggil String.format, tapi saya akan ngeri jika rangkaian sederhana melakukannya. Mengapa kompiler menggunakan string format yang kemudian harus diuraikan?
Jon Skeet
Saya menggunakan "bytecode" di mana saya seharusnya mengatakan "kode biner." Ketika semuanya turun ke jmps dan movs, itu mungkin kode yang sama persis.
Ya - Jake itu.
0

Pertimbangkan "hello".concat( "world!" )untuk menggunakan sejumlah kecil string dalam rangkaian. Ini bisa lebih baik untuk kinerja daripada pendekatan lain.

Jika Anda memiliki lebih dari 3 string, daripada mempertimbangkan menggunakan StringBuilder, atau hanya String, tergantung pada kompiler yang Anda gunakan.

Sasa
sumber