Seberapa buruk itu memanggil println () sering daripada merangkai string bersama dan memanggilnya sekali?

23

Saya tahu output ke konsol adalah operasi yang mahal. Untuk kepentingan keterbacaan kode kadang-kadang ada baiknya memanggil fungsi untuk menampilkan teks dua kali, daripada memiliki string teks yang panjang sebagai argumen.

Misalnya, seberapa efisien untuk dimiliki

System.out.println("Good morning.");
System.out.println("Please enter your name");

vs.

System.out.println("Good morning.\nPlease enter your name");

Dalam contoh perbedaannya hanya satu panggilan ke println()tetapi bagaimana jika itu lebih?

Pada catatan terkait, pernyataan yang melibatkan pencetakan teks dapat terlihat aneh saat melihat kode sumber jika teks yang akan dicetak panjang. Dengan asumsi teks itu sendiri tidak dapat dibuat lebih pendek, apa yang bisa dilakukan? Haruskah ini menjadi kasus di mana beberapa println()panggilan dilakukan? Seseorang pernah memberi tahu saya sebaris kode tidak boleh lebih dari 80 karakter (IIRC) jadi apa yang akan Anda lakukan

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Apakah hal yang sama berlaku untuk bahasa seperti C / C ++ karena setiap kali data ditulis ke aliran output, pemanggilan sistem harus dilakukan dan prosesnya harus menuju ke mode kernel (yang sangat mahal)?

Celeritas
sumber
Meskipun ini adalah kode yang sangat kecil, saya harus mengatakan saya telah bertanya-tanya hal yang sama. Akan menyenangkan untuk menentukan jawaban untuk ini sekali dan untuk semua
Simon Forsberg
@ SimonAndréForsberg Saya tidak yakin apakah itu berlaku untuk Java karena berjalan pada mesin virtual, tetapi dalam bahasa tingkat yang lebih rendah seperti C / C ++ Saya akan membayangkan itu akan mahal karena setiap kali sesuatu menulis ke aliran output, panggilan sistem harus dibuat.
Ada ini untuk dipertimbangkan juga: stackoverflow.com/questions/21947452/…
hjk
1
Saya harus mengatakan bahwa saya tidak mengerti maksudnya di sini. Ketika berinteraksi dengan pengguna melalui terminal, saya tidak bisa membayangkan masalah kinerja karena biasanya tidak banyak yang bisa dicetak. Dan aplikasi dengan GUI atau aplikasi web harus menulis ke file log (biasanya menggunakan kerangka kerja).
Andy
1
Jika Anda mengucapkan selamat pagi, Anda melakukannya sekali atau dua kali sehari. Optimalisasi bukan masalah. Jika ini sesuatu yang lain, Anda perlu profil untuk mengetahui apakah itu masalah. Kode yang saya kerjakan pada logging memperlambat kode menjadi tidak dapat digunakan kecuali jika Anda membangun buffer multi-baris dan membuang teks dalam satu panggilan.
mattnz

Jawaban:

29

Ada dua 'kekuatan' di sini, dalam ketegangan: Kinerja vs Keterbacaan.

Mari kita atasi masalah ketiga terlebih dahulu, garis panjang:

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Cara terbaik untuk menerapkan ini dan menjaga kewaspadaan, adalah dengan menggunakan penggabungan string:

System.out.println("Good morning everyone. I am here today to present you "
                 + "with a very, very lengthy sentence in order to prove a "
                 + "point about how it looks strange amongst other code.");

Rangkaian string-konstan akan terjadi pada waktu kompilasi, dan tidak akan berpengaruh pada kinerja sama sekali. Garis-garisnya dapat dibaca, dan Anda bisa melanjutkan.

Sekarang, tentang:

System.out.println("Good morning.");
System.out.println("Please enter your name");

vs.

System.out.println("Good morning.\nPlease enter your name");

Opsi kedua secara signifikan lebih cepat. Saya akan menyarankan tentang 2X secepat .... mengapa?

Karena 90% (dengan margin kesalahan yang luas) dari pekerjaan tidak terkait dengan membuang karakter ke output, tetapi diperlukan overhead untuk mengamankan output untuk menulis padanya.

Sinkronisasi

System.outadalah a PrintStream. Semua implementasi Java yang saya ketahui, secara internal menyinkronkan PrintStream: Lihat kode di GrepCode! .

Apa artinya ini untuk kode Anda?

Ini berarti bahwa setiap kali Anda menelepon System.out.println(...)Anda menyinkronkan model memori Anda, Anda sedang memeriksa dan menunggu kunci. Utas lain yang memanggil System.out juga akan dikunci.

Dalam aplikasi single-threaded dampaknya System.out.println()sering dibatasi oleh kinerja IO sistem Anda, seberapa cepat Anda dapat menulis ke file. Dalam aplikasi multithreaded, penguncian bisa menjadi masalah daripada IO.

Pembilasan

Setiap println memerah . Ini menyebabkan buffer dibersihkan dan memicu penulisan tingkat Konsol ke buffer. Jumlah upaya yang dilakukan di sini tergantung pada implementasi, tetapi, secara umum dipahami bahwa kinerja flush hanya sebagian kecil terkait dengan ukuran buffer yang disiram. Ada overhead signifikan terkait dengan flush, di mana buffer memori ditandai sebagai kotor, mesin Virtual melakukan IO, dan sebagainya. Mengalami overhead itu sekali, bukan dua kali, merupakan pengoptimalan yang jelas.

Beberapa angka

Saya mengumpulkan tes kecil berikut:

public class ConsolePerf {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            benchmark("Warm " + i);
        }
        benchmark("real");
    }

    private static void benchmark(String string) {
        benchString(string + "short", "This is a short String");
        benchString(string + "long", "This is a long String with a number of newlines\n"
                  + "in it, that should simulate\n"
                  + "printing some long sentences and log\n"
                  + "messages.");

    }

    private static final int REPS = 1000;

    private static void benchString(String name, String value) {
        long time = System.nanoTime();
        for (int i = 0; i < REPS; i++) {
            System.out.println(value);
        }
        double ms = (System.nanoTime() - time) / 1000000.0;
        System.err.printf("%s run in%n    %12.3fms%n    %12.3f lines per ms%n    %12.3f chars per ms%n",
                name, ms, REPS/ms, REPS * (value.length() + 1) / ms);

    }


}

Kode ini relatif sederhana, berulang kali mencetak string pendek, atau panjang ke output. String panjang memiliki beberapa baris baru di dalamnya. Ini mengukur berapa lama untuk mencetak masing-masing 1.000 iterasi.

Jika saya menjalankannya di prompt perintah unix (Linux), dan mengarahkan ulang STDOUTke /dev/null, dan mencetak hasil yang sebenarnya STDERR, saya dapat melakukan hal berikut:

java -cp . ConsolePerf > /dev/null 2> ../errlog

Output (dalam errlog) terlihat seperti:

Warm 0short run in
           7.264ms
         137.667 lines per ms
        3166.345 chars per ms
Warm 0long run in
           1.661ms
         602.051 lines per ms
       74654.317 chars per ms
Warm 1short run in
           1.615ms
         619.327 lines per ms
       14244.511 chars per ms
Warm 1long run in
           2.524ms
         396.238 lines per ms
       49133.487 chars per ms
.......
Warm 99short run in
           1.159ms
         862.569 lines per ms
       19839.079 chars per ms
Warm 99long run in
           1.213ms
         824.393 lines per ms
      102224.706 chars per ms
realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Apa artinya ini? Saya ulangi 'bait' terakhir:

realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Ini berarti bahwa, untuk semua maksud dan tujuan, meskipun garis 'panjang' sekitar 5 kali lebih lama, dan berisi beberapa baris baru, dibutuhkan output yang sama panjangnya dengan garis pendek.

Jumlah karakter per detik untuk jangka panjang adalah 5 kali lebih banyak, dan waktu yang berlalu hampir sama .....

Dengan kata lain, kinerja Anda berskala relatif terhadap jumlah cetak yang Anda miliki, bukan apa yang mereka cetak.

Pembaruan: Apa yang terjadi jika Anda mengarahkan ulang ke file, alih-alih ke / dev / null?

realshort run in
           2.592ms
         385.815 lines per ms
        8873.755 chars per ms
reallong run in
           2.686ms
         372.306 lines per ms
       46165.955 chars per ms

Ini jauh lebih lambat, tetapi proporsinya hampir sama ....

rolfl
sumber
Menambahkan beberapa angka kinerja.
rolfl
Anda juga harus mempertimbangkan masalah yang "\n"mungkin bukan terminator garis kanan. printlnakan secara otomatis mengakhiri baris dengan karakter yang tepat, tetapi menempel \nke string Anda secara langsung dapat menyebabkan masalah. Jika Anda ingin melakukannya dengan benar, Anda mungkin perlu menggunakan pemformatan string atau line.separatorproperti sistem . printlnjauh lebih bersih.
user2357112 mendukung Monica
3
Ini semua analisis yang bagus jadi +1 pasti, tapi saya berpendapat bahwa setelah Anda berkomitmen untuk output konsol, perbedaan kinerja kecil ini terbang keluar jendela. Jika algoritme program Anda berjalan lebih cepat daripada mengeluarkan hasilnya (pada tingkat kecil keluaran ini), Anda dapat mencetak setiap karakter satu per satu dan tidak melihat perbedaannya.
David Harkness
Saya percaya ini adalah perbedaan antara Java dan C / C ++ bahwa output disinkronkan. Saya mengatakan ini karena saya ingat menulis program multithreaded dan mengalami masalah dengan output kacau jika utas yang berbeda mencoba menulis untuk menulis ke konsol. Adakah yang bisa memverifikasi ini?
6
Penting juga untuk diingat bahwa tidak ada kecepatan yang penting sama sekali ketika diletakkan tepat di sebelah fungsi yang menunggu input pengguna.
vmrob
2

Saya tidak berpikir memiliki banyak printlns adalah masalah desain sama sekali. Cara saya melihatnya adalah bahwa ini jelas dapat dilakukan dengan penganalisa kode statis jika itu benar-benar masalah.

Tapi itu bukan masalah karena kebanyakan orang tidak melakukan IO seperti ini. Ketika mereka benar-benar perlu melakukan banyak IO, mereka menggunakan yang di-buffer (BufferedReader, BufferedWriter, dll.) Ketika input di-buffer, Anda akan melihat bahwa kinerjanya cukup mirip, sehingga Anda tidak perlu khawatir memiliki sekelompok printlnatau sedikit println.

Jadi untuk menjawab pertanyaan aslinya. Saya akan mengatakan, tidak buruk jika Anda menggunakannya printlnuntuk mencetak beberapa hal karena kebanyakan orang akan menggunakannya println.

InformedA
sumber
1

Dalam bahasa tingkat yang lebih tinggi seperti C dan C ++, ini kurang masalah daripada di Jawa.

Pertama-tama, C dan C ++ mendefinisikan penggabungan string waktu kompilasi, sehingga Anda dapat melakukan sesuatu seperti:

std::cout << "Good morning everyone. I am here today to present you with a very, "
    "very lengthy sentence in order to prove a point about how it looks strange "
    "amongst other code.";

Dalam kasus seperti itu, menggabungkan string bukan hanya optimasi yang bisa Anda lakukan, biasanya (dll) bergantung pada kompiler yang akan dibuat. Sebaliknya, secara langsung diperlukan oleh standar C dan C ++ (fase 6 terjemahan: "Token string yang berdekatan disatukan.").

Meskipun dengan mengorbankan sedikit kompleksitas tambahan dalam kompiler dan implementasi, C dan C ++ melakukan sedikit lebih banyak untuk menyembunyikan kompleksitas menghasilkan output secara efisien dari programmer. Java jauh lebih mirip bahasa rakitan - setiap panggilan untuk System.out.printlnmenerjemahkan lebih langsung ke panggilan ke operasi yang mendasarinya untuk menulis data ke konsol. Jika Anda ingin buffering untuk meningkatkan efisiensi, itu harus disediakan secara terpisah.

Ini berarti, misalnya, bahwa dalam C ++, menulis ulang contoh sebelumnya, untuk sesuatu seperti ini:

std::cout << "Good morning everyone. I am here today to present you with a very, ";
std::cout << "very lengthy sentence in order to prove a point about how it looks ";       
std::cout << "strange amongst other code.";

... biasanya 1 tidak akan berpengaruh pada efisiensi. Setiap penggunaan couthanya akan menyetor data ke buffer. Buffer itu akan mengalir ke aliran yang mendasarinya ketika buffer terisi, atau kode mencoba membaca input dari penggunaan (seperti dengan std::cin).

iostreams juga memiliki sync_with_stdioproperti yang menentukan apakah output dari iostreams disinkronkan dengan input gaya-C (mis., getchar). Secara default sync_with_stdiodisetel ke true, jadi jika, misalnya, Anda menulis std::cout, lalu membaca getchar, data yang Anda tulis coutakan memerah ketika getchardipanggil. Anda dapat mengatur sync_with_stdioke false untuk menonaktifkan itu (biasanya dilakukan untuk meningkatkan kinerja).

sync_with_stdiojuga mengontrol tingkat sinkronisasi antara utas. Jika sinkronisasi diaktifkan (default) menulis ke iostream dari beberapa utas dapat menghasilkan data dari utas yang disisipkan, tetapi mencegah segala kondisi balapan. TKI, program Anda akan berjalan dan menghasilkan output, tetapi jika lebih dari satu utas menulis ke aliran pada satu waktu, pencampuran yang acak dari data dari utas yang berbeda biasanya akan membuat keluaran yang cukup berguna.

Jika Anda mematikan sinkronisasi, maka sinkronisasi akses dari banyak utas juga menjadi tanggung jawab Anda. Penulisan bersamaan dari beberapa utas dapat / akan menyebabkan perlombaan data, yang berarti kode memiliki perilaku yang tidak ditentukan.

Ringkasan

C ++ default untuk upaya menyeimbangkan kecepatan dengan keamanan. Hasilnya cukup berhasil untuk kode single-threaded, tetapi kurang begitu untuk kode multi-threaded. Kode multithreaded biasanya perlu memastikan bahwa hanya satu utas yang menulis ke aliran sekaligus untuk menghasilkan keluaran yang bermanfaat.


1. Dimungkinkan untuk mematikan buffering untuk stream, tetapi sebenarnya melakukan hal itu sangat tidak biasa, dan ketika / jika seseorang melakukannya, itu mungkin karena alasan yang cukup spesifik, seperti memastikan bahwa semua output ditangkap segera meskipun ada efek pada kinerja . Bagaimanapun, ini hanya terjadi jika kode melakukannya secara eksplisit.

Jerry Coffin
sumber
13
" Dalam bahasa tingkat yang lebih tinggi seperti C dan C ++, ini kurang masalah daripada di Jawa. " - apa? C dan C ++ adalah bahasa level yang lebih rendah dari Java. Selain itu, Anda lupa terminator saluran Anda.
user2357112 mendukung Monica
1
Sepanjang saya menunjukkan dasar objektif untuk Jawa menjadi bahasa tingkat rendah. Tidak yakin dengan terminator apa yang Anda bicarakan.
Jerry Coffin
2
Java juga melakukan penggabungan waktu kompilasi. Sebagai contoh, "2^31 - 1 = " + Integer.MAX_VALUEdisimpan sebagai string yang diinternir tunggal (JLS Sec 3.10.5 dan 15.28 ).
200_sukses
2
@ 200_success: Java yang melakukan penggabungan string pada waktu kompilasi tampaknya turun ke §15.18.1: "Objek String baru dibuat (§12.5) kecuali jika ekspresi adalah ekspresi konstanta waktu kompilasi (§15.28)." Hal ini tampaknya memungkinkan tetapi tidak mengharuskan penggabungan dilakukan pada waktu kompilasi. Yaitu, hasilnya harus baru dibuat jika input tidak konstanta waktu kompilasi, tetapi tidak ada persyaratan yang dibuat di kedua arah jika mereka mengkompilasi konstanta waktu. Untuk meminta penggabungan waktu kompilasi, Anda harus membaca (tersirat) "jika" benar-benar berarti "jika dan hanya jika".
Jerry Coffin
2
@ Phoshi: Coba dengan sumber daya bahkan tidak mirip dengan RAII. RAII memungkinkan kelas untuk mengelola sumber daya, tetapi mencoba dengan sumber daya memerlukan kode klien untuk mengelola sumber daya. Fitur-fitur (abstraksi, lebih akurat) yang dimiliki seseorang dan yang lainnya tidak sepenuhnya relevan - sebenarnya, itulah yang membuat satu bahasa memiliki tingkat yang lebih tinggi dari yang lain.
Jerry Coffin
1

Sementara kinerja tidak benar-benar masalah di sini, keterbacaan yang buruk dari banyak printlnpernyataan menunjuk ke aspek desain yang hilang.

Mengapa kita menulis urutan banyak printlnpernyataan? Jika itu hanya satu blok teks tetap, seperti --helpteks dalam perintah konsol, akan jauh lebih baik untuk memilikinya sebagai sumber daya yang terpisah dan membacanya dan menulisnya ke layar berdasarkan permintaan.

Tetapi biasanya itu adalah campuran dari bagian dinamis dan statis. Katakanlah kita memiliki beberapa data pesanan kosong di satu sisi, dan beberapa bagian teks statis tetap di sisi lain, dan hal-hal ini harus dicampur bersama untuk membentuk lembar konfirmasi pesanan. Sekali lagi, juga dalam kasus ini, lebih baik untuk memiliki file teks ressource yang terpisah: ressource akan menjadi template, yang mengandung beberapa jenis simbol (placeholder), yang diganti saat runtime oleh data pesanan aktual.

Pemisahan bahasa pemrograman dari bahasa alami memiliki banyak keuntungan - di antaranya adalah internasionalisasi: Anda mungkin harus menerjemahkan teks jika Anda ingin menjadi multilangual dengan perangkat lunak Anda. Juga, mengapa langkah kompilasi diperlukan jika Anda hanya ingin memiliki koreksi tekstual, katakan perbaiki salah eja.

rplantiko
sumber