Waarom wil je het doorspoelen van stdout vermijden?

Ik kwam een vraag tegen in Codereview , en in één antwoord was de feedback om vermijd std::endl omdat het de stream leegmaakt. Het volledige citaat is:

Ik “zou adviseren om std::endl in het algemeen te vermijden. Samen met het schrijven van een nieuwe- lijn naar de stream, het spoelt de stream. Je wilt de nieuwe regel, maar wilt bijna nooit de stream doorspoelen, dus het is over het algemeen beter om gewoon een \ n te schrijven. In het zeldzame geval dat u de spoeling echt wilt, doe dit dan expliciet: std::cout << "\n" << std::flush;.

De poster deed het leg dit niet uit, noch in de post, noch in de commentaren. Dus mijn vraag is simpelweg deze:

Waarom wil je doorspoelen voorkomen?

Wat me nog nieuwsgieriger maakte, was dat de poster zegt dat het zeer zeldzaam is dat je wilt doorspoelen. Ik heb geen probleem om situaties voor te stellen waarin je doorspoelen wilt vermijden, maar ik dacht nog steeds dat je dat in het algemeen wel zou willen flush wanneer u een nieuwe regel afdrukt. Is dat tenslotte niet de reden waarom std::endl in de eerste plaats doorspoelt?

Om de sluitende stemmen van tevoren te becommentariëren:

Ik beschouw deze mening niet als gebaseerd. Waar u de voorkeur aan geeft, kan op meningen zijn gebaseerd, maar er zijn objectieve redenen om rekening mee te houden. De antwoorden tot dusver bewijzen dit. Doorspoelen beïnvloedt de prestaties.

Reacties

Antwoord

Het korte en eenvoudige antwoord is dat het gebruik van std::endl de uitvoer met een enorme marge kan en zal vertragen. In feite ben ik er redelijk van overtuigd dat std::endl verantwoordelijk is voor de meeste van het idee dat C ++ iostreams aanzienlijk langzamer zijn dan C-stijl I / O.

Beschouw bijvoorbeeld een programma als dit:

#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; } 

Met standaarduitvoer omgeleid naar een bestand, levert dit de volgende resultaten op:

 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 

Door std::endl te gebruiken, vertraagde het programma in dit geval met een factor 20. Als je kortere strings schreef, vertraging kan / zou zelfs nog groter zijn.

Er zijn een paar gevallen waarin je echt en echt een stream handmatig wilt doorspoelen – maar eerlijk gezegd zijn ze er maar heel weinig tussen.

Meestal moet een stream worden doorgespoeld (je drukt bijvoorbeeld een prompt af en wacht dan op wat invoer), het zal automatisch gebeuren, tenzij je dingen hebt gebruikt als std::tie en / of std::sync_with_stdio om dat te voorkomen.

Dat laat slechts een klein aantal echt ongebruikelijke situaties over waarin u goede reden om een stream handmatig door te spoelen. Dergelijke gevallen zijn zo zeldzaam dat het de moeite waard is om std::flush te gebruiken wanneer ze zich voordoen, om het voor iedereen die de code leest duidelijk te maken dat u de stream opzettelijk doorspoelt (en vaker dan niet, verdient waarschijnlijk ook een opmerking over waarom dit een van de zeldzame gevallen is waarin het doorspoelen van de stream echt zinvol is).

Antwoord

Elke keer dat een proces output produceert, moet het een functie aanroepen die werkelijk het werk doet. In de meeste gevallen is die functie uiteindelijk write(2). Op een multitasking-besturingssysteem zal de aanroep van write() in de kernel terechtkomen, die het proces moet stoppen, de I / O moet afhandelen, andere dingen moet doen terwijl eventuele blokkades worden verwijderd, het in de wachtrij en laat het weer draaien wanneer de tijd daar is. Gezamenlijk kunt u al die activiteit systeemoproepoverhead aanroepen. Als dat veel klinkt, is het dat ook.

Het doorspoelen van een gebufferde stream * na het schrijven van een kleine hoeveelheid gegevens of het hebben van helemaal geen buffer veroorzaakt die overhead elke keer dat je het doet:

 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)  

Dit is hoe het werd gedaan in de allereerste dagen totdat iemand erachter kwam dat het een veel systeemtijd. De overhead kon laag worden gehouden door output in een buffer te verzamelen totdat deze vol was of het programma besloot dat het onmiddellijk verzonden moest worden.(Misschien wilt u het laatste doen als u “sporadisch uitvoer produceert die moet worden gezien of verbruikt.) Door een flush aan het einde van elke regel te vermijden, vermindert het aantal systeemoproepen en de gemaakte overhead:

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

* Merk op dat het concept van standaarduitvoer een bestandsdescriptor is die is gekoppeld aan een proces en een bekend nummer gegeven. Dit verschilt van de stdout gedefinieerd door C, C ++ en anderen, die identificaties zijn voor implementaties van een gebufferde stream die volledig in gebruikersland leven en naar de standaard uitvoer. De write() systeemaanroep wordt niet gebufferd.

Reacties

  • Of en wanneer doorspoelen hangt af van de context waarin die uitvoer zal worden gebruikt. Een doorvoergeoriënteerd programma mag alleen doorspoelen als de buffers vol zijn. Een latentiegevoelig programma zou vaker moeten doorspoelen. Als de uitvoer bijvoorbeeld naar een console gaat, Spoel na elke nieuwe regel. Interactieve programmas (die een invoerprompt tonen) zouden onmiddellijk moeten worden leeggemaakt, zelfs als de regel nog niet vol is.
  • ” als de uitvoer naar een console gaat, zou nog steeds doorspoelen na elke nieuwe regel. ” Dat is waar, maar als de uitvoer naar een console gaat, is er een automatische spoeling na elke nieuwe regel. Dat hoeft niet expliciet te doen.
  • @amon Je zou nog steeds opeenvolgende output willen samenvoegen, want op tijd doorspoelen, dat wil zeggen voordat je om input vraagt en zonder achterstallige vertraging, is voldoende. Toegegeven, het is beter om een keer te vaak te spoelen dan te veel vertraging te hebben bij het tonen van de output, of te veel werk te investeren in het optimaliseren van …
  • Een truc die soms door de bufferers wordt gebruikt, is om een timer als er nieuwe dingen komen, en doorspoelen als de timer afloopt.

Answer

Waarom flush moet worden vermeden:

Omdat IO het beste werkt wanneer het besturingssysteem kan werken met relatief grote hoeveelheden gegevens. Regelmatige doorspoelingen met kleine hoeveelheden gegevens veroorzaken vertragingen, soms zeer aanzienlijk.

Waarom zou u bijna nooit handmatig moeten doorspoelen:

Er zijn automatische doorspoelingen die in de meeste gevallen van toepassing zijn. Als een programma bijvoorbeeld naar de console schrijft, spoelt het systeem standaard na elke nieuwe regel. Of als u naar een bestand schrijft, worden de gegevens geschreven zodra er voldoende gegevens zijn om in één keer te schrijven, en ook wanneer het bestand wordt gesloten.

Wanneer je moet handmatig spoelen:

Als je de uitvoer expliciet onmiddellijk moet bijwerken. Voorbeeld: als u een spinner of voortgangsbalk maakt die herhaaldelijk de huidige regel overschrijft. Of als u naar een bestand uitvoert en echt wilt dat het bestand op specifieke momenten wordt bijgewerkt.

Opmerkingen

  • Het systeem kan geen gebruikersbuffers leegmaken. Of onderbrengt u bibliotheken, met name de standaardbibliotheek, ook onder ” systeem “? Natuurlijk zijn stdin en stdout over het algemeen gekoppeld als ze allebei dezelfde console gebruiken.
  • Ja, Ik dacht niet dat het ‘ een goed idee zou zijn om te gedetailleerd in te gaan op de manier waarop dingen zijn verdeeld tussen gebruikersruimtebibliotheken en kernelruimte.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *