Dlaczego chcesz uniknąć wypłukiwania standardowego wyjścia?

W Codereview natknąłem się na pytanie, a jedna z odpowiedzi dotyczyła unikaj std::endl, ponieważ powoduje opróżnianie strumienia. Pełny cytat to:

Ogólnie radzę unikać std::endl. Oprócz pisania nowego do strumienia, opróżnia strumień. Chcesz nowej linii, ale prawie nigdy nie chcesz opróżniać strumienia, więc generalnie lepiej jest po prostu napisać \ n. W rzadkich przypadkach, gdy naprawdę chcesz rzucić, zrób to wyraźnie: std::cout << "\n" << std::flush;.

Plakat nie wyjaśniam tego ani w poście, ani w komentarzach. Moje pytanie jest więc po prostu takie:

Dlaczego chcesz uniknąć spłukiwania?

Jeszcze bardziej zaciekawiło mnie to, że na plakacie jest napisane, że bardzo rzadko chcesz się zarumienić. Nie mam problemu z wyobrażeniem sobie sytuacji, w których chcesz uniknąć zaczerwienienia, ale nadal myślałem, że ogólnie chciałbyś flush podczas drukowania nowej linii. Czy przecież nie jest to powód, dla którego std::endl jest w pierwszej kolejności opróżniany?

Chciałem tylko skomentować z wyprzedzeniem głosy końcowe:

Nie uważam, aby ta opinia była oparta. To, co wolisz, może być oparte na opiniach, ale istnieją obiektywne powody, które należy wziąć pod uwagę. Dowodzą tego dotychczasowe odpowiedzi. Płukanie wpływa na wydajność.

Komentarze

Odpowiedź

Krótka i prosta odpowiedź polega na tym, że użycie std::endl może spowolnić drukowanie o ogromny margines. W rzeczywistości jestem dość przekonany, że std::endl jest odpowiedzialny za większość poglądu, że C ++ iostreams są znacznie wolniejsze niż I / O w stylu C.

Na przykład rozważmy taki program:

#include <iostream> #include <string> #include <sstream> #include <time.h> #include <iomanip> #include <algorithm> #include <iterator> #include <stdio.h> char fmt[] = "%s\n"; static const int count = 3000000; static char const *const string = "This is a string."; static std::string s = std::string(string) + "\n"; void show_time(void (*f)(), char const *caption) { clock_t start = clock(); f(); clock_t ticks = clock()-start; std::cerr << std::setw(30) << caption << ": " << (double)ticks/CLOCKS_PER_SEC << "\n"; } void use_printf() { for (int i=0; i<count; i++) printf(fmt, string); } void use_puts() { for (int i=0; i<count; i++) puts(string); } void use_cout() { for (int i=0; i<count; i++) std::cout << string << "\n"; } void use_cout_unsync() { std::cout.sync_with_stdio(false); for (int i=0; i<count; i++) std::cout << string << "\n"; std::cout.sync_with_stdio(true); } void use_stringstream() { std::stringstream temp; for (int i=0; i<count; i++) temp << string << "\n"; std::cout << temp.str(); } void use_endl() { for (int i=0; i<count; i++) std::cout << string << std::endl; } void use_fill_n() { std::fill_n(std::ostream_iterator<char const *>(std::cout, "\n"), count, string); } void use_write() { for (int i = 0; i < count; i++) std::cout.write(s.data(), s.size()); } int main() { show_time(use_printf, "Time using printf"); show_time(use_puts, "Time using puts"); show_time(use_cout, "Time using cout (synced)"); show_time(use_cout_unsync, "Time using cout (un-synced)"); show_time(use_stringstream, "Time using stringstream"); show_time(use_endl, "Time using endl"); show_time(use_fill_n, "Time using fill_n"); show_time(use_write, "Time using write"); return 0; } 

Przy standardowym wyjściu przekierowanym do pliku daje to następujące wyniki:

 Time using printf: 0.208539 Time using puts: 0.103065 Time using cout (synced): 0.241377 Time using cout (un-synced): 0.181853 Time using stringstream: 0.223617 Time using endl: 4.32881 Time using fill_n: 0.209951 Time using write: 0.102781 

Użycie std::endl spowolniło program w tym przypadku o współczynnik około 20. Jeśli napisałeś krótsze ciągi, spowolnienie mogłoby / byłoby jeszcze większe.

Jest kilka przypadków, w których naprawdę i naprawdę chcesz ręcznie opróżnić strumień – ale szczerze mówiąc, jest ich bardzo niewiele.

W większości przypadków strumień musi zostać opróżniony (np. drukujesz monit, a następnie czekasz na jakieś dane wejściowe), dzieje się to automatycznie, chyba że użyłeś takich rzeczy jak std::tie i / lub std::sync_with_stdio, aby temu zapobiec.

To pozostawia tylko niewielką liczbę naprawdę nietypowych sytuacji, w których masz dobry powód, aby ręcznie przepłukać strumień. Takie przypadki są na tyle rzadkie, że dobrze jest użyć std::flush, gdy już się zdarzają, aby każdy czytający kod wiedział, że „przepłukujesz strumień celowo (i częściej niż nie, prawdopodobnie również zasługuje na komentarz dotyczący dlaczego jest to jeden z rzadkich przypadków, gdy opróżnianie strumienia naprawdę ma sens).

Odpowiedź

Za każdym razem, gdy proces generuje wynik, musi wywołać funkcję, która faktycznie wykonuje pracę. W większości przypadków ta funkcja to ostatecznie write(2). W wielozadaniowym systemie operacyjnym wywołanie write() spowoduje pułapkę w jądrze, które musi zatrzymać proces, obsłużyć I / O, robić inne rzeczy, gdy wszystkie blokady są usuwane, umieszcza w kolejce gotowości i uruchom ponownie, gdy nadejdzie czas. Całość można nazwać łącznie obciążeniem wywołań systemowych . Jeśli to wydaje się dużo, to tak jest.

Opróżnianie buforowanego strumienia * po zapisaniu niewielkiej ilości danych lub brak bufora powoduje to za każdym razem, gdy to robisz:

 1\n (System call that writes two bytes) 2\n (System call that writes two bytes) 3\n (System call that writes two bytes) 4\n (System call that writes two bytes) 5\n (System call that writes two bytes)  

W ten sposób robiono to w pierwszych dniach, dopóki ktoś nie zorientował się, że pali dużo czasu systemowego. Narzut można ograniczyć, gromadząc dane wyjściowe w buforze, aż do jego zapełnienia lub gdy program zdecyduje, że należy je natychmiast wysłać.(Możesz chcieć zrobić to drugie, jeśli „produkujesz sporadycznie dane wyjściowe, które trzeba zobaczyć lub skonsumować). Unikanie koloru na końcu każdej linii zmniejsza liczbę wywołań systemowych i poniesione narzuty:

 1\n 2\n 3\n 4\n 5\n (Flush) (System call that writes ten bytes)  

* Zwróć uwagę, że pojęcie standardowego wyjścia to deskryptor pliku powiązany z procesem i podany jest dobrze znany numer. Różni się to od stdout zdefiniowanych w C, C ++ i innych, które są identyfikatorami implementacji buforowanego strumienia, który żyje całkowicie w przestrzeni użytkownika i zapisuje do standardowe wyjście. Wywołanie systemowe write() nie jest buforowane.

Komentarze

  • Czy i kiedy opróżniać zależy od kontekstu, w którym dane wyjście zostanie użyte. Program zorientowany na przepustowość powinien opróżniać się tylko wtedy, gdy jego bufory są pełne. Program wrażliwy na opóźnienia powinien opróżniać się częściej. Np. jeśli wyjście trafia do konsoli, należy spłukuje po każdym nowym wierszu. Programy interaktywne (które wyświetlają podpowiedzi) powinny natychmiast opróżnić się, nawet jeśli linia nie jest jeszcze pełna.
  • ” jeśli wyjście trafia do konsoli, nadal będzie spłukiwał po każdym znaku nowej linii. ” Prawda, ale jeśli wyjście trafia do konsoli, następuje automatyczne usunięcie po każdym znaku nowej linii. Nie ma potrzeby robić tego wprost.
  • @amon Nadal chciałbyś łączyć kolejne wyjścia, ponieważ opróżnianie w odpowiednim czasie, czyli przed zapytaniem o dane wejściowe i bez zaległego opóźnienia, jest wystarczające. Trzeba przyznać, że lepiej jest przepłukiwać raz za często niż mieć zbyt duże opóźnienie w wyświetlaniu wyniku lub inwestować zbyt dużo pracy w optymalizację …
  • Jedną sztuczką używaną czasami przez bufory jest ponowne / rozpoczęcie licznik czasu, gdy nadejdą nowe rzeczy, i opróżnij, gdy skończy się czas.

Odpowiedz

Dlaczego należy unikać flush:

Ponieważ IO działa najlepiej, gdy system operacyjny może pracować ze stosunkowo dużymi ilościami danych. Regularne opróżnianie niewielkich ilości danych powoduje spowolnienia, czasami bardzo znaczące.

Dlaczego prawie nigdy nie należy spłukiwać ręcznie:

Istnieją automatyczne opróżnienia, które obejmują większość przypadków użycia. Na przykład, jeśli program pisze do konsoli, system domyślnie opróżnia po każdym nowym wierszu. Lub jeśli piszesz do pliku, dane są zapisywane, gdy jest wystarczająca ilość danych do zapisania naraz, a także gdy plik jest zamknięty.

Kiedy powinieneś opróżnić ręcznie:

Jeśli jawnie potrzebujesz natychmiast zaktualizować wyjście. Przykład: jeśli utworzysz pokrętło lub pasek postępu, który wielokrotnie nadpisuje bieżącą linię. Lub jeśli wyprowadzasz do pliku i naprawdę chcesz, aby plik był aktualizowany w określonych momentach.

Komentarze

  • System nie może opróżnić żadnych buforów użytkowników. Czy może subskrybujesz biblioteki, zwłaszcza bibliotekę standardową, również w systemie ” „? Oczywiście stdin i stdout są generalnie połączone, przynajmniej jeśli używają tej samej konsoli.
  • Tak, Nie ' nie sądzę, że byłoby dobrym pomysłem zbyt szczegółowe opisywanie podziału rzeczy między biblioteki przestrzeni użytkownika i przestrzeń jądra.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *