Println () işlevini bir araya getirmek, dizeleri birleştirmek ve bir kez çağırmaktan ne kadar kötü?


23

Konsolun çıkışının pahalı bir işlem olduğunu biliyorum. Kod okunabilirliği açısından bazen, uzun bir metin dizesini bağımsız değişken olarak kullanmak yerine, iki kez metin çıktısı alma işlevini çağırmak iyidir.

Örneğin, ne kadar az verimli olması

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

vs.

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

Örnekte, fark yalnızca bir çağrıdır, println()fakat ya daha fazlasıysa?

İlgili bir notta, yazdırılacak metin uzunsa kaynak kodunu görüntülerken metin yazdırmayı içeren ifadeler garip görünebilir. Metnin kendisinin kısaltılmadığını varsayarsak ne yapılabilir? Bu, birden fazla println()görüşmenin yapıldığı bir durum mu olmalı? Birisi bir keresinde bana bir kod satırının 80 karakterden (IIRC) fazla olmaması gerektiğini söyledi.

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.");

Aynı veri C / C ++ gibi diller için de geçerlidir, çünkü her veri akışı çıkış akışına yazıldığında bir sistem çağrısı yapılmalı ve işlem çekirdek moduna (çok maliyetli) geçmeli mi?


Bu çok küçük bir kod olsa da, aynı şeyi merak ettiğimi söylemeliyim. Bunun cevabını bir kez ve herkes için belirlemek güzel olurdu
Simon Forsberg

@ SimonAndréForsberg Sanal bir makinede çalıştığı için Java'ya uygulanabilir olup olmadığından emin değilim, ancak C / C ++ gibi daha düşük seviyelerde, bir şeyin bir çıktı akışına her yazışında olduğu gibi pahalı olacağını hayal ediyorum, bir sistem çağrısı yapılmalı.

Dikkate alınması gereken bir şey var: stackoverflow.com/questions/21947452/…
hjk

1
Buradaki noktayı göremediğimi söylemeliyim. Terminal üzerinden bir kullanıcıyla etkileşim kurarken, herhangi bir performans sorunu hayal edemiyorum, çünkü genellikle yazdırılacak çok fazla şey yok. GUI veya webapp içeren uygulamalar bir günlük dosyasına (genellikle bir çerçeve kullanarak) yazmalıdır.
Andy

1
Günaydın diyorsanız, günde bir veya iki kez yapın. Optimizasyon bir endişe değil. Başka bir şey varsa, bunun bir sorun olup olmadığını bilmek zorunda olmanız gerekir. Kayıt sırasında çalıştığım kod, çok satırlı bir arabellek oluşturup metni bir çağrıya dökmediğiniz sürece kodu kullanılamaz duruma getirir.
mattnz

Yanıtlar:


29

Burada gergin iki güç vardır: Performansa Karşı Okunabilirlik.

Önce üçüncü sorunu çözelim, ama uzun çizgiler:

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.");

Bunu yapmanın ve okunabilirliği korumanın en iyi yolu, dize bitiştirme kullanmaktır:

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.");

String-sabit birleştirme derleme zamanında gerçekleşecek ve performans üzerinde hiçbir etkisi olmayacak. Satırlar okunabilir ve devam edebilirsiniz.

Şimdi, hakkında:

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

vs.

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

İkinci seçenek önemli ölçüde daha hızlı. 2X hakkında hızlı bir şekilde önereceğim .... neden?

Çünkü çalışmanın% 90'ı (geniş bir hata payı ile) karakterleri çıktıya dökmekle ilgili değildir, ancak çıktının yazması için sabitlenmesi için ek yüke ihtiyaç vardır.

Senkronizasyon

System.outbir PrintStream. Tanıdığım tüm Java uygulamaları, PrintStream'i dahili olarak senkronize eder: GrepCode! .

Bunun kodunuz için anlamı nedir?

Her aradığınızda System.out.println(...)bellek modelinizi senkronize ettiğinizde kontrol ediyor ve kilit bekliyorsunuz demektir. System.out'u çağıran diğer başlıklar da kilitlenir.

Tek iş parçacıklı uygulamalarda System.out.println() genellikle sisteminizin IO performansı ile sınırlıdır, dosyaya ne kadar hızlı yazabilirsiniz. Çok iş parçacıklı uygulamalarda, kilitleme IO'dan daha önemli bir konu olabilir.

Flushing

Her baskı temizlendi . Bu, arabelleklerin temizlenmesine neden olur ve arabelleklere Konsol düzeyinde bir yazı tetikler. Burada yapılan çaba miktarı uygulamaya bağlıdır, ancak genel olarak, yıkama işleminin performansının yalnızca temizlenen tamponun boyutuyla ilgili küçük bir kısım olduğu anlaşılmaktadır. Bellek arabelleklerinin kirli olarak işaretlendiği yıkama ile ilgili önemli bir ek yük var, Sanal makine G / Ç vb. Bu ek yükün iki kez yerine bir kez yapılması açık bir optimizasyondur.

Bazı sayılar

Aşağıdaki küçük testi bir araya getirdim:

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);

    }


}

Kod nispeten basittir, art arda çıktısı almak için kısa veya uzun bir dize yazdırır. Uzun String'in içinde birden fazla yeni satır var. Her birinin 1000 yinelemesini yazdırmanın ne kadar sürdüğünü ölçer.

Bunu unix (Linux) komut isteminde çalıştırırsam, yönlendirme STDOUTyapar /dev/nullve gerçek sonuçları STDERRbasarsam, aşağıdakileri yapabilirim:

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

Çıktı (errlog'da) şöyle görünür:

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

Ne anlama geliyor? Son 'stanza'yı tekrar edeyim:

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

Bu, tüm amaç ve amaçlar için, 'uzun' satırın yaklaşık 5 kat daha uzun olmasına ve birden çok yeni satır içermesine rağmen, kısa satır olarak çıktısının alınması kadar uzun sürdüğü anlamına gelir.

Uzun vadede saniye başına düşen karakter sayısı 5 kat fazla, geçen süre yaklaşık aynı ...

Diğer bir deyişle, performans göreli ölçekler numara var printlns arasında değil, neyi onlar yazdırın.

Güncelleme: / dev / null yerine bir dosyaya yönlendirirseniz ne olur?

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

Çok daha yavaş, ama oranlar aynı.


Bazı performans numaraları eklendi.
rolfl

Ayrıca "\n"doğru hat sonlandırıcısı olmayabilir problemi de göz önünde bulundurmalısınız . printlndoğru karakter (ler) ile satırı otomatik olarak sonlandırır, ancak a'yı \ndizenize doğrudan yapıştırmak sorunlara neden olabilir. Doğru yapmak istiyorsanız, dize biçimlendirmesini veya line.separatorsistem özelliğini kullanmanız gerekebilir . printlnçok daha temiz.
user2357112 Monica'yı destekliyor

3
Bunların hepsi harika bir analiz, yani kesinlikle + 1, ancak bir kez çıktı elde etmeye karar verdiğinizde, bu küçük performans farklarının pencereden fırladığını savunuyorum. Programınızın algoritması, sonuçların çıktısından daha hızlı çalışıyorsa (bu küçük çıktı düzeyinde), her bir karakteri tek tek yazdırabilir ve farkı göremezsiniz.
David Harkness

Bunun Java ile C / C ++ arasında çıktı senkronize edildiğindeki bir fark olduğuna inanıyorum. Bunu söylüyorum çünkü çok iş parçacıklı bir program yazıyor ve farklı iplikler konsola yazmayı denerse bozuk çıktıyla ilgili sorunlarım var. Bunu kimse doğrulayabilir mi?

6
Kullanıcı girişini bekleyen işlevin hemen yanına getirildiğinde, bu hızlardan hiçbirinin önemli olmadığını hatırlamak önemlidir .
vmrob

2

Bir demet olması gerektiğini sanmıyorum println bir tasarım sorunu . Gördüğüm yol, bunun gerçekten bir sorun olması halinde statik kod analizcisiyle açıkça yapılabileceği.

Ancak bu bir sorun değil çünkü çoğu insan böyle bir şey yapmıyor. Gerçekten çok fazla GÇ yapmaları gerektiğinde, giriş arabelleğe alındığında arabelleğe alınmış olanları (BufferedReader, BufferedWriter, vb.) Kullanırlar. Performansın yeterince benzer olduğunu göreceksiniz. demet printlnya da azprintln .

Yani orijinal soruyu cevaplamak için. printlnÇoğu insanın kullandığı gibi bir şeyler yazdırırsanız , fena değil derim println.


1

C ve C ++ gibi yüksek seviyeli dillerde, bu Java'dan daha az problemdir.

Öncelikle, C ve C ++ derleme zamanı dize bitiştirmeyi tanımlar, böylece şöyle bir şey yapabilirsiniz:

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.";

Böyle bir durumda, dizgiyi birleştirmek sadece hemen hemen yapabileceğiniz bir optimizasyon değildir, genellikle (vb.) Derleyiciye bağlıdır. Aksine, doğrudan C ve C ++ standartlarına göre gereklidir (çevirinin 6. aşaması: "Bitişik dize değişmez belirteçleri bitiştirilir.").

Derleyici ve uygulamada biraz daha fazla karmaşıklık pahasına olmasına rağmen, C ve C ++, programcıdan verimli bir şekilde çıktı üretme karmaşıklığını gizlemek için biraz daha fazlasını yapar. Java, derleme diline çok benzer - her çağrı System.out.printlndoğrudan konsola veri yazmak için yapılan işlemleri doğrudan çağrıya çevirir. Verimliliği artırmak için tamponlama yapmak istiyorsanız, bunun ayrıca sağlanması gerekir.

Bu, örneğin, C ++ 'da önceki örneği yeniden yazmanın şöyle bir şey olduğu anlamına gelir:

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.";

... normalde 1'in verimlilik üzerinde neredeyse hiçbir etkisi olmazdı . Her kullanım coutbasitçe verileri bir tampon belleğe depolar. Tampon doldurulduğunda ya da kod kullanımdaki (okuma ile olduğu gibi std::cin) girdiyi okumaya çalıştığında, bu tampon temeldeki akışa temizlenirdi .

iostreams ayrıca sync_with_stdio, iostreams'tan gelen çıkışın C tarzı giriş ile senkronize edilip edilmeyeceğini belirleyen bir özelliğe sahiptir (örn getchar.). Varsayılan sync_with_stdioolarak true değerine ayarlanır, bu nedenle, örneğin, yazarsanız std::cout, ardından okursanız getchar, yazdığınız verileri çağrıldığında couttemizlenir getchar. Bunu sync_with_stdiodevre dışı bırakmak için false olarak ayarlayabilirsiniz (genellikle performansı artırmak için yapılır).

sync_with_stdioayrıca dişler arasında bir senkronizasyon derecesini de kontrol eder. Eşitleme açıksa (varsayılan), birden fazla iş parçacığından bir ilk akışa yazma iş parçacığı verilerinin araya girmesine neden olabilir, ancak yarış koşullarını önler. IOW, programınız çıkacak ve çıktısını üretecek, ancak bir seferde birden fazla iş parçacığı bir akışa yazarsa, farklı iş parçacıklarından gelen verilerin rasgele birbirine karışması tipik olarak çıktıyı oldukça işe yaramaz hale getirecektir.

Eğer açarsanız kapalı senkronizasyonu, birden çok iş parçacığı sonra senkronize erişim sıra tamamen sizin sorumluluğunuz olur. Birden fazla iş parçacığından eşzamanlı yazma, kodun tanımsız davranışa sahip olduğu anlamına gelen bir veri yarışına yol açabilir / açacaktır.

özet

C ++ varsayılan hızda güvenlik dengeleme denemesi için varsayılan. Sonuç, tek iş parçacıklı kod için oldukça başarılı, ancak çok iş parçacıklı kod için daha az başarılıdır. Çok iş parçacıklı kod tipik olarak, yararlı çıktı üretmek için bir seferde yalnızca bir iş parçacığının bir akışa yazdığından emin olunması gerekir.


1. Bir akış için arabelleğe alma özelliğini kapatmak mümkündür, ancak aslında bunu yapmak oldukça sıra dışıdır ve / eğer biri bunu yaparsa, muhtemelen performans üzerindeki etkisine rağmen tüm çıktının hemen yakalanmasını sağlamak gibi oldukça spesifik bir sebepten dolayıdır. . Her durumda, bu yalnızca kod açıkça yaparsa gerçekleşir.


13
" C ve C ++ gibi daha yüksek seviyelerde, bu Java'dan daha az problemdir. " - ne? C ve C ++, Java'dan daha düşük seviyeli dillerdir. Ayrıca, sonlandırıcılarını unuttun.
user2357112 Monica destekler

1
Boyunca, Java'nın alt seviye dil olduğu nesnel temeli işaret ediyorum. Hangi hat sonlandırıcılarından bahsettiğinden emin değilim.
Jerry Coffin,

2
Java ayrıca derleme zamanı birleştirmeyi de yapar. Örneğin, "2^31 - 1 = " + Integer.MAX_VALUEtek bir dahili dize olarak saklanır (JLS Sec 3.10.5 ve 15.28 ).
200_success

2
@ 200_success: Java, derleme zamanında dize birleştirmeyi yapıyor görünüyor: §15.18.1: "İfade bir derleme zamanı sabiti ifadesi (§15.28) olmadığı sürece, String nesnesi yeni yaratıldı (§12.5)." Bu izin veriyor ancak birleştirme işleminin derleme zamanında yapılmasını gerektirmiyor. Yani, eğer girdiler derleme zamanı sabitleri değilse, sonuç yeni oluşturulmalıdır, fakat derleme zamanı sabitleri ise her iki yönde de bir zorunluluk yoktur. Derleme zamanı birleştirmeyi gerektirmek için, "ima edilmiş" ise "gerçekten" olduğu takdirde "eğer ve sadece" deyin.
Jerry Coffin,

2
@ Phoshi: Kaynakları deneyin, RAII'ye bile belli değil. RAII, sınıfın kaynakları yönetmesine izin verir, ancak kaynakları dene, kaynakları yönetmek için istemci kodunu gerektirir. Birinin sahip olduğu ve diğerinin eksik olduğu özellikler (soyutlamalar, daha doğru bir şekilde) tamamen ilgili - aslında, bir dili diğerinden daha yüksek seviyeye getiren şey tam olarak onlar.
Jerry Coffin,

1

Performans burada gerçekten bir sorun olmasa da, bir çok printlnifadenin okunaklı olması eksik tasarım yönüne işaret ediyor.

Neden birçok printlnifade dizisi yazıyoruz ? Sadece bir sabit metin bloğu --helpolsaydı, bir konsol komutundaki bir metin gibi, ayrı bir kaynak olarak kullanmak ve onu okumak ve istek üzerine ekrana yazmak çok daha iyi olurdu.

Ancak, genellikle dinamik ve statik parçaların bir karışımıdır. Diyelim ki bir yandan çıplak düzen verilerimiz, diğer yandan da sabit statik metin kısımları ve bir sipariş onay sayfası oluşturmak için bunların karıştırılması gerekiyor. Yine, bu durumda, ayrıca, ayrı bir kaynak metin dosyasına sahip olmak daha iyidir: Kaynak, çalışma zamanında gerçek sipariş verileri ile değiştirilen bazı semboller (yer tutucuları) içeren bir şablon olacaktır.

Programlama dilinin doğal dilden ayrılması birçok avantaja sahiptir - bunlar arasında uluslararasılaşma: Yazılımınızla çok dilli olmak istiyorsanız metni çevirmeniz gerekebilir. Ayrıca, neden yalnızca bir metin düzeltmesi yapmak istiyorsanız bir derleme adımı gerekli olmalı, bazı yazım hatalarını düzeltin.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.