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)?
Odpowiedzi:
W napięciu występują dwie „siły”: wydajność vs. czytelność.
Najpierw jednak zajmiemy się trzecim problemem, długimi liniami:
Najlepszym sposobem na wdrożenie tego i zachowanie czytelności jest użycie konkatenacji ciągów znaków:
Łą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:
vs.
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.out
jestPrintStream
. 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:
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ę
STDOUT
do/dev/null
i wydrukuję rzeczywiste wynikiSTDERR
, mogę wykonać następujące czynności:Dane wyjściowe (w dzienniku błędów) wyglądają następująco:
Co to znaczy? Powtórzę ostatnią zwrotkę:
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?
Jest o wiele wolniejszy, ale proporcje są prawie takie same ....
źródło
"\n"
może nie być właściwym terminatorem linii.println
automatycznie zakończy linię z odpowiednim znakiem (znakami), ale bezpośrednie przyklejenie\n
do łańcucha może powodować problemy. Jeśli chcesz to zrobić poprawnie, może być konieczne użycie formatowania łańcucha lubline.separator
właściwości systemowej .println
jest znacznie czystszy.Nie sądzę, żeby posiadanie kilku
println
s 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
println
lub kilkaprintln
.Aby odpowiedzieć na pierwotne pytanie. Powiedziałbym, że nieźle, jeśli użyjesz
println
do wydrukowania kilku rzeczy, których większość ludzi by użyłaprintln
.źródło
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:
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.println
przekł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:
... normalnie 1 nie miałby prawie żadnego wpływu na wydajność. Każde użycie
cout
po 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
).iostream
s mają równieżsync_with_stdio
właściwość, która określa, czy wyjście z iostreams jest zsynchronizowane z wejściem w stylu C (npgetchar
.). Domyślniesync_with_stdio
ustawiona jest wartość true, więc jeśli na przykład piszeszstd::cout
, a następnie czytaszgetchar
, dane, do których napisałeś,cout
zostaną opróżnione pogetchar
wywołaniu. Możesz ustawić wartośćsync_with_stdio
false, aby to wyłączyć (zwykle robi się to w celu poprawy wydajności).sync_with_stdio
kontroluje 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.
źródło
"2^31 - 1 = " + Integer.MAX_VALUE
jest przechowywany jako pojedynczy ciąg wewnętrzny (JLS Sec 3.10.5 i 15.28 ).Podczas gdy wydajność nie jest tak naprawdę problemem, zła czytelność wielu
println
stwierdzeń wskazuje na brak aspektu projektowego.Dlaczego piszemy sekwencję wielu
println
instrukcji? Gdyby był to tylko jeden stały blok tekstowy, jak--help
tekst 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.
źródło