Proč se chcete vyhnout vyprázdnění stdout?

Narazil jsem na otázku v Codereview a v jedné odpovědi byla zpětná vazba vyhněte se std::endl, protože to propláchne stream. Celá nabídka je:

Doporučuji vyhnout se std::endl obecně. Spolu s napsáním nového- řádek k proudu, propláchne proud. Chcete nový řádek, ale téměř nikdy nechcete proud propláchnout, takže je obecně lepší napsat pouze \ n. Ve výjimečných případech, kdy skutečně chcete flush, udělejte to explicitně: std::cout << "\n" << std::flush;.

Plakát udělal nevysvětlete to, ani v příspěvku, ani v komentářích. Moje otázka tedy zní jednoduše takto:

Proč se chcete vyhnout spláchnutí?

Ještě zvědavější mě bylo, že plakát říká, že je velmi vzácné, že chcete spláchnout. Nemám problém si představit situace, kdy se chcete vyhnout spláchnutí, ale pořád jsem si myslel, že byste obecně chtěli propláchnout, když tisknete nový řádek. Koneckonců, není to důvod, proč se std::endl zaplavuje na prvním místě?

Jen předem komentuji blízké hlasy:

Tento názor nepovažuji za založený. Které byste měli upřednostňovat, mohou být založeny na názorech, ale je třeba vzít v úvahu objektivní důvody. Dosavadní odpovědi to dokazují. Proplachování ovlivňuje výkon.

Komentáře

Odpověď

Krátká a jednoduchá odpověď je to, že použití std::endl může a zpomalí výstup s obrovským náskokem. Ve skutečnosti jsem rozumně přesvědčen, že std::endl je zodpovědný za většinu představy, že C ++ iostreamy jsou podstatně pomalejší než I / O ve stylu C.

Zvažte například takový 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; } 

Se standardním výstupem přesměrovaným do souboru to způsobí následující výsledky:

 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 

Použití std::endl v tomto případě zpomalilo program zhruba o 20 procent. Pokud jste psali kratší řetězce, zpomalení by mohlo / bylo by ještě větší.

Existuje několik případů, kdy opravdu a skutečně chcete proud vypustit ručně – ale je jich opravdu málo a daleko od sebe.

Stream je většinou třeba vyprázdnit (např. vytisknout výzvu a počkat na nějaký vstup), stane se to automaticky, pokud nepoužíváte věci jako std::tie a / nebo std::sync_with_stdio tomu zabránit.

To ponechává jen malé množství skutečně neobvyklých situací, kdy máte dobrý důvod k ručnímu vypláchnutí proudu. Takové případy jsou natolik vzácné, že stojí za to použít std::flush, když k nim dojde, aby bylo každému, kdo čte kód, zřejmé, že proud úmyslně (a častěji) vyprazdňujete než ne, pravděpodobně si zaslouží komentář o proč toto je jeden z mála případů, kdy má proplachování proudu opravdu smysl).

Odpovědět

Pokaždé, když proces produkuje výstup, musí zavolat funkci, která skutečně dělá práci. Ve většině případů je tato funkce nakonec write(2). V multitaskingovém operačním systému bude volání write() zachyceno do jádra, které musí zastavit proces, zpracovat I / O, dělat jiné věci, zatímco jsou odstraněna všechna zablokování, dát je ve frontě připraveno a znovu naběhne, až přijde čas. Dohromady můžete všechny tyto aktivity volat režie systémových volání . Pokud to zní jako hodně, je to tak.

Vypláchnutí proudu s vyrovnávací pamětí * po zapsání malého množství dat nebo bez jakéhokoli vyrovnávací paměti se objeví pokaždé, když to uděláte:

 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)  

Takto se to dělalo v prvních dnech, dokud někdo nezjistil, že to pálí hodně systémového času. Režii bylo možné udržovat na nízké úrovni akumulací výstupu do vyrovnávací paměti, dokud nebyl plný nebo dokud program nerozhodl, že musí být odeslán okamžitě.(To druhé možná budete chtít udělat, pokud produkujete výstup sporadicky, který je třeba vidět nebo spotřebovat.) Vyhnutí se vyprázdnění na konci každého řádku sníží počet systémových volání a vzniknou režii:

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

* Všimněte si, že koncept standardního výstupu je deskriptor souboru spojený s procesem a dostal dobře známé číslo. To se liší od stdout definovaného C, C ++ a dalšími, což jsou identifikátory pro implementaci proudu ve vyrovnávací paměti, který žije zcela v uživatelské zemi a zapisuje do standardní výstup. write() systémové volání není ukládáno do vyrovnávací paměti.

Komentáře

  • Zda a kdy má být vyprázdněn záleží na kontextu, ve kterém bude tento výstup použit. Program orientovaný na propustnost by měl být vyprázdněn, pouze když jsou jeho vyrovnávací paměti plné. Program citlivý na latenci by měl vyprázdnit častěji. Např. pokud výstup jde na konzolu, měli byste Po každém novém řádku vypláchnu. Interaktivní programy (které ukazují určitou vstupní výzvu) by měly okamžitě propláchnout, i když řádek ještě není plný.
  • “ pokud výstup jde do konzoly, vy bude stále flush po každém novém řádku. “ Je pravda, ale pokud výstup přejde na konzolu, dojde po každém novém řádku k automatickému flush. Není třeba to dělat výslovně.
  • @amon Stále byste chtěli spojit po sobě jdoucí výstup, protože včasné proplachování, což znamená před vyžádáním vstupu a bez zpoždění, je dostačující. Je pravda, že je lepší propláchnout jednou příliš často, než mít příliš mnoho zpoždění při zobrazování výstupu nebo investovat příliš mnoho práce do optimalizace …
  • Jeden trik, který někdy používají vyrovnávací paměti, je znovu / spustit časovač, když přijdou nové věci, a vyprázdnění, když časovač vyprší.

Odpověď

Proč je třeba se vyhnout vyprázdnění:

Protože IO funguje nejlépe, když operační systém dokáže pracovat s relativně velkým množstvím dat. Pravidelné proplachy s malým množstvím dat způsobují zpomalení, někdy velmi výrazné.

Proč byste téměř nikdy neměli proplachovat ručně:

Existují automatické návaly, které pokrývají většinu případů použití. Například pokud program zapisuje do konzoly, systém se ve výchozím nastavení vyprázdní po každém novém řádku. Nebo pokud zapisujete do souboru, data se zapisují, jakmile je k dispozici dostatek dat k okamžitému zápisu, a také při zavření souboru.

Když měli byste propláchnout ručně:

Pokud výslovně potřebujete okamžitě aktualizovat výstup. Příklad: pokud vytvoříte číselník nebo ukazatel průběhu, který opakovaně přepíše aktuální řádek. Nebo pokud odesíláte výstup do souboru a opravdu chcete, aby byl soubor aktualizován v určitých okamžicích.

Komentáře

  • Systém nemůže vyprázdnit žádné uživatelské vyrovnávací paměti. Nebo zahrajete knihovny, zejména standardní knihovnu, také pod “ systém „? Samozřejmě jsou stdin a stdout obecně spojeny přinejmenším, pokud oba používají stejnou konzolu.
  • Ano, Nemyslel jsem si ‚, že by bylo dobré zacházet příliš podrobně o tom, jak se věci dělí mezi knihovnami uživatelského prostoru a prostorem jádra.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *