Jak źle nazywa się println () często niż łączenie ciągów razem i wywoływanie go raz?

23

Wiem, że wyjście na konsolę to kosztowna operacja. W trosce o czytelność kodu miło jest czasem wywołać funkcję, która wyśle ​​tekst dwukrotnie, zamiast mieć długi ciąg tekstu jako argument.

Na przykład, o ile mniej wydajny

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

vs.

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

W tym przykładzie różnica polega tylko na jednym wywołaniu, println()ale co, jeśli jest więcej?

W powiązanej notatce stwierdzenia dotyczące drukowania tekstu mogą wyglądać dziwnie podczas przeglądania kodu źródłowego, jeśli tekst do wydrukowania jest długi. Zakładając, że sam tekst nie może być skrócony, co można zrobić? Czy powinien to być przypadek, w którym println()można wykonać wiele połączeń? Ktoś kiedyś powiedział mi, że linia kodu nie powinna zawierać więcej niż 80 znaków (IIRC), więc co byś zrobił

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

Czy to samo dotyczy języków takich jak C / C ++, ponieważ za każdym razem, gdy dane są zapisywane w strumieniu wyjściowym, należy wykonać wywołanie systemowe, a proces musi przejść do trybu jądra (co jest bardzo kosztowne)?

Celeritas
źródło
Mimo że jest to bardzo mało kodu, muszę powiedzieć, że zastanawiałem się nad tym samym. Byłoby miło ustalić odpowiedź na to raz na zawsze
Simon Forsberg
@ SimonAndréForsberg Nie jestem pewien, czy dotyczy Java, ponieważ działa na maszynie wirtualnej, ale w językach niższego poziomu, takich jak C / C ++, wyobrażam sobie, że byłoby to kosztowne, ponieważ za każdym razem, gdy coś pisze do strumienia wyjściowego, wywołanie systemowe musi zostać zrobione.
Trzeba to również rozważyć: stackoverflow.com/questions/21947452/...
hjk
1
Muszę powiedzieć, że nie widzę tutaj sensu. Podczas interakcji z użytkownikiem przez terminal nie wyobrażam sobie żadnych problemów z wydajnością, ponieważ zazwyczaj nie ma tak dużo do wydrukowania. Aplikacje z graficznym interfejsem użytkownika lub aplikacją internetową powinny zapisywać do pliku dziennika (zwykle przy użyciu frameworka).
Andy
1
Jeśli mówisz dzień dobry, robisz to raz lub dwa razy dziennie. Optymalizacja nie stanowi problemu. Jeśli to coś innego, musisz się profilować, aby wiedzieć, czy to problem. Kod, nad którym pracuję przy logowaniu, spowalnia kod do bezużyteczności, chyba że zbudujesz bufor wieloliniowy i zrzucisz tekst w jednym wywołaniu.
mattnz

Odpowiedzi:

29

W napięciu występują dwie „siły”: wydajność vs. czytelność.

Najpierw jednak zajmiemy się trzecim problemem, długimi liniami:

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

Najlepszym sposobem na wdrożenie tego i zachowanie czytelności jest użycie konkatenacji ciągów znaków:

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

Łączenie ze stałym ciągiem nastąpi w czasie kompilacji i nie będzie miało żadnego wpływu na wydajność. Linie są czytelne i możesz po prostu przejść dalej.

Teraz o:

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

vs.

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

Druga opcja jest znacznie szybsza. Zasugeruję około 2 razy tak szybko .... dlaczego?

Ponieważ 90% (z szerokim marginesem błędu) pracy nie jest związane z zrzucaniem znaków na dane wyjściowe, ale jest narzucone, aby zabezpieczyć dane wyjściowe przed ich zapisaniem.

Synchronizacja

System.outjest PrintStream. Wszystkie implementacje Java, które znam, wewnętrznie synchronizują PrintStream: zobacz kod na GrepCode! .

Co to oznacza dla twojego kodu?

Oznacza to, że za każdym razem, gdy dzwonisz System.out.println(...), synchronizujesz model pamięci, sprawdzasz i czekasz na blokadę. Wszelkie inne wątki wywołujące System.out również zostaną zablokowane.

W aplikacjach jednowątkowych wpływ System.out.println()jest często ograniczony przez wydajność IO twojego systemu, jak szybko możesz pisać do pliku. W aplikacjach wielowątkowych blokowanie może być większym problemem niż we / wy.

Rumienić się

Każdy println jest opróżniany . Powoduje to wyczyszczenie buforów i wyzwala zapis na poziomie konsoli do buforów. Ilość włożonego tutaj wysiłku zależy od implementacji, ale ogólnie rozumie się, że wydajność opróżniania jest tylko w niewielkiej części związana z wielkością opróżnianego bufora. Istnieje znaczny narzut związany z opróżnianiem, gdzie bufory pamięci są oznaczone jako brudne, maszyna wirtualna wykonuje operacje we / wy itd. Poniesienie tego obciążenia raz, a nie dwa razy, jest oczywistą optymalizacją.

Niektóre liczby

Złożyłem następujący mały test:

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 jest względnie prosty, wielokrotnie wypisuje krótki lub długi ciąg znaków do wydrukowania. Długi łańcuch zawiera wiele nowych linii. Mierzy, ile czasu zajmuje wydrukowanie 1000 iteracji każdego z nich.

Jeśli uruchomię go w wierszu polecenia unix (Linux) i przekieruję STDOUTdo /dev/nulli wydrukuję rzeczywiste wyniki STDERR, mogę wykonać następujące czynności:

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

Dane wyjściowe (w dzienniku błędów) wyglądają następująco:

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

Co to znaczy? Powtórzę ostatnią zwrotkę:

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

Oznacza to, że dla wszystkich celów i celów, mimo że „długa” linia jest około 5-krotnie dłuższa i zawiera wiele nowych linii, wydrukowanie jest tak samo długie jak krótkiej linii.

W długim okresie liczba znaków na sekundę jest 5 razy większa, a upływający czas jest mniej więcej taki sam .....

Innymi słowy, wydajność skaluje się w stosunku do liczby wydruków, które masz, a nie tego , co drukują.

Aktualizacja: Co się stanie, jeśli przekierujesz do pliku zamiast do / 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

Jest o wiele wolniejszy, ale proporcje są prawie takie same ....

rolfl
źródło
Dodano kilka numerów wyników.
rolfl
Musisz także wziąć pod uwagę problem, który "\n"może nie być właściwym terminatorem linii. printlnautomatycznie zakończy linię z odpowiednim znakiem (znakami), ale bezpośrednie przyklejenie \ndo łańcucha może powodować problemy. Jeśli chcesz to zrobić poprawnie, może być konieczne użycie formatowania łańcucha lub line.separatorwłaściwości systemowej . printlnjest znacznie czystszy.
user2357112 obsługuje Monikę
3
To wszystko jest świetna analiza, więc na pewno +1, ale argumentowałbym, że gdy zdecydujesz się na konsolę, te niewielkie różnice w wydajności znikną za oknem. Jeśli algorytm twojego programu działa szybciej niż wypisywanie wyników (przy tak niskim poziomie wyjściowym), możesz wydrukować każdy znak jeden po drugim i nie zauważając różnicy.
David Harkness
Wierzę, że jest to różnica między Javą a C / C ++, że dane wyjściowe są zsynchronizowane. Mówię to, ponieważ przypominam sobie pisanie programu wielowątkowego i problemy z zniekształconym wyjściem, jeśli różne wątki próbują pisać, aby zapisać na konsoli. Czy ktoś może to zweryfikować?
6
Ważne jest również, aby pamiętać, że żadna z tych prędkości w ogóle nie ma znaczenia, gdy zostanie ustawiona obok funkcji, która czeka na dane użytkownika.
vmrob
2

Nie sądzę, żeby posiadanie kilku printlns było w ogóle problemem projektowym. Widzę to tak, że można to wyraźnie zrobić za pomocą statycznego analizatora kodów, jeśli to naprawdę problem.

Ale to nie jest problem, ponieważ większość ludzi nie robi takich zamówień. Kiedy naprawdę muszą wykonać wiele operacji we / wy, używają buforowanych (BufferedReader, BufferedWriter itp.), Gdy dane wejściowe są buforowane, zobaczysz, że wydajność jest na tyle podobna, że ​​nie musisz się martwić o posiadanie kilka printlnlub kilka println.

Aby odpowiedzieć na pierwotne pytanie. Powiedziałbym, że nieźle, jeśli użyjesz printlndo wydrukowania kilku rzeczy, których większość ludzi by użyła println.

Poinformowano
źródło
1

W językach wyższych poziomów, takich jak C i C ++, jest to mniejszy problem niż w Javie.

Po pierwsze, C i C ++ definiują konkatenację ciągów znaków w czasie kompilacji, więc możesz zrobić coś takiego:

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

W takim przypadku konkatenacja ciągu nie jest tylko optymalizacją, na którą możesz właściwie polegać, zwykle (itp.) Od kompilatora. Raczej jest to bezpośrednio wymagane przez standardy C i C ++ (faza 6 tłumaczenia: „Sąsiadujące ze sobą dosłowne ciągi znaków są łączone”).

Choć odbywa się to kosztem dodatkowej złożoności kompilatora i implementacji, C i C ++ robią jeszcze więcej, aby ukryć złożoność wydajnego tworzenia danych wyjściowych przez programistę. Java jest bardziej podobna do języka asemblera - każde wywołanie System.out.printlnprzekłada się znacznie bardziej bezpośrednio na wywołanie operacji bazowej w celu zapisania danych na konsoli. Jeśli chcesz buforować w celu poprawy wydajności, musisz to podać osobno.

Oznacza to na przykład, że w C ++, przepisując poprzedni przykład, na coś takiego:

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

... normalnie 1 nie miałby prawie żadnego wpływu na wydajność. Każde użycie coutpo prostu zdeponowałoby dane w buforze. Bufor ten zostałby opróżniony do strumienia leżącego u podstaw, gdy bufor się zapełni lub kod spróbuje odczytać dane wejściowe z użycia (np. Za pomocą std::cin).

iostreams mają również sync_with_stdiowłaściwość, która określa, czy wyjście z iostreams jest zsynchronizowane z wejściem w stylu C (np getchar.). Domyślnie sync_with_stdioustawiona jest wartość true, więc jeśli na przykład piszesz std::cout, a następnie czytasz getchar, dane, do których napisałeś, coutzostaną opróżnione po getcharwywołaniu. Możesz ustawić wartość sync_with_stdiofalse, aby to wyłączyć (zwykle robi się to w celu poprawy wydajności).

sync_with_stdiokontroluje również stopień synchronizacji między wątkami. Jeśli synchronizacja jest włączona (domyślnie) zapisywanie do iostream z wielu wątków może spowodować przeplatanie danych z wątków, ale zapobiega warunkom wyścigu. IOW, twój program będzie działał i generował dane wyjściowe, ale jeśli więcej niż jeden wątek zapisuje jednocześnie strumień, dowolne mieszanie danych z różnych wątków zazwyczaj czyni dane wyjściowe bardzo bezużytecznymi.

Po włączeniu się do synchronizacji, a następnie synchronizację dostęp z wielu wątków staje się całkowicie odpowiedzialny za dobrze. Równoczesne zapisy z wielu wątków mogą prowadzić do wyścigu danych, co oznacza, że ​​kod ma niezdefiniowane zachowanie.

Podsumowanie

C ++ domyślnie próbuje wyważyć prędkość z bezpieczeństwem. Wynik jest dość udany w przypadku kodu jednowątkowego, ale mniej w przypadku kodu wielowątkowego. Kod wielowątkowy zwykle musi zapewniać, że tylko jeden wątek zapisuje strumień jednocześnie, aby uzyskać użyteczne dane wyjściowe.


1. Możliwe jest wyłączenie buforowania dla strumienia, ale w rzeczywistości jest to dość niezwykłe, a kiedy / jeśli ktoś to zrobi, to prawdopodobnie z dość konkretnego powodu, takiego jak zapewnienie natychmiastowego przechwytywania wszystkich danych wyjściowych pomimo wpływu na wydajność . W każdym razie dzieje się tak tylko wtedy, gdy kod robi to wprost.

Jerry Coffin
źródło
13
W językach wyższego poziomu, takich jak C i C ++, jest to mniejszy problem niż w Javie. ” - co? C i C ++ są językami niższego poziomu niż Java. Zapomniałeś również terminatorów linii.
user2357112 obsługuje Monikę
1
W całym tekście wskazuję na obiektywną podstawę dla języka Java, który jest językiem niższego poziomu. Nie jestem pewien, o jakich terminatorach linii mówisz.
Jerry Coffin
2
Java wykonuje także konkatenację w czasie kompilacji. Na przykład "2^31 - 1 = " + Integer.MAX_VALUEjest przechowywany jako pojedynczy ciąg wewnętrzny (JLS Sec 3.10.5 i 15.28 ).
200_success
2
@ 200_success: Java wykonująca konkatenację łańcucha w czasie kompilacji wydaje się sprowadzać do §15.18.1: „Obiekt String jest nowo utworzony (§12.5), chyba że wyrażenie jest wyrażeniem stałym w czasie kompilacji (§15.28).” Wydaje się, że pozwala to, ale nie wymaga, aby konkatenacja została wykonana w czasie kompilacji. Oznacza to, że wynik musi zostać nowo utworzony, jeśli dane wejściowe nie są stałymi czasami kompilacji, ale nie są wymagane w żadnym kierunku, jeśli są stałymi czasami kompilacji. Aby wymagać konkatenacji w czasie kompilacji, musisz przeczytać jej (domyślnie) „jeśli”, co naprawdę znaczy „wtedy i tylko wtedy”.
Jerry Coffin
2
@ Phoshi: Spróbuj z zasobami nie jest nawet trochę podobny do RAII. RAII pozwala klasie zarządzać zasobami, ale próba z zasobami wymaga kodu klienta do zarządzania zasobami. Funkcje (abstrakcje, dokładniej), które ma jeden, a drugi brak, są całkowicie istotne - w rzeczywistości są one dokładnie tym, co sprawia, że ​​jeden język jest wyższy od drugiego.
Jerry Coffin
1

Podczas gdy wydajność nie jest tak naprawdę problemem, zła czytelność wielu printlnstwierdzeń wskazuje na brak aspektu projektowego.

Dlaczego piszemy sekwencję wielu printlninstrukcji? Gdyby był to tylko jeden stały blok tekstowy, jak --helptekst w poleceniu konsoli, znacznie lepiej byłoby mieć go jako osobny zasób i wczytać go i zapisać na ekranie na żądanie.

Ale zwykle jest to mieszanka części dynamicznych i statycznych. Załóżmy, że mamy z jednej strony pewne dane na temat samego zamówienia, a z drugiej strony stałe części tekstu statycznego, i te rzeczy należy wymieszać, aby utworzyć arkusz potwierdzenia zamówienia. Ponownie, również w tym przypadku, lepiej jest mieć osobny plik tekstowy z zasobami: Zasób byłby szablonem zawierającym pewnego rodzaju symbole (symbole zastępcze), które w czasie wykonywania są zastępowane przez rzeczywiste dane zamówienia.

Oddzielenie języka programowania od języka naturalnego ma wiele zalet - między innymi jest to internacjonalizacja: Być może będziesz musiał przetłumaczyć tekst, jeśli chcesz stać się wielojęzyczny ze swoim oprogramowaniem. Ponadto, dlaczego krok kompilacji powinien być konieczny, jeśli chcesz mieć tylko korektę tekstową, na przykład napraw błąd pisowni.

rplantiko
źródło