Varför vill du undvika att spola ut stdout?

Jag snubblade på en fråga i Codereview , och i ett svar var feedbacken att undvik std::endl eftersom det spolar strömmen. Hela citatet är:

Jag rekommenderar att man undviker std::endl i allmänhet. Tillsammans med att skriva en ny- linje till strömmen, den spolar strömmen. Du vill ha den nya linjen, men nästan aldrig vill spola strömmen, så det är i allmänhet bättre att bara skriva en \ n. Vid det sällsynta tillfället att du verkligen vill spola, gör det uttryckligen: std::cout << "\n" << std::flush;.

Affischen gjorde inte förklara detta, varken i inlägget eller kommentarerna. Så min fråga är helt enkelt den här:

Varför vill du undvika att spola?

Det som gjorde mig ännu mer nyfiken var att affischen säger att det är väldigt sällsynt att du vill spola. Jag har inga problem att föreställa mig situationer där du vill undvika att spola, men jag trodde ändå att du i allmänhet skulle vilja spola när du skriver ut en ny rad. Det är ju inte orsaken till att std::endl spolas i första hand?

Bara för att kommentera de nära rösterna i förväg:

Jag anser inte att detta yttrande är baserat. Vilket du föredrar kan vara meningsbaserat men det finns objektiva skäl att ta hänsyn till. Svaren hittills bevisar detta. Spolning påverkar prestanda.

Kommentarer

Svar

Det korta och enkla svaret är att användning av std::endl kan och kommer att sakta ner produktionen med stor marginal. Jag är faktiskt rimligt övertygad om att std::endl är ansvarig för de flesta tankarna att C ++ iostreams är väsentligt långsammare än C-style I / O.

Tänk till exempel på ett sådant 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; } 

Med standardutdata omdirigerad till en fil ger detta följande resultat:

 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 

Användning av std::endl bromsade programmet med en faktor på cirka 20 i det här fallet. Om du skrev kortare strängar, avmattning kan / skulle vara ännu större.

Det finns några fall där du verkligen och verkligen vill spola en ström manuellt – men de är ärligt talat ganska långt ifrån.

De flesta gånger måste en ström spolas (t.ex. om du skriver ut en uppmaning och väntar sedan på en del inmatning) kommer det ”att ske automatiskt om du inte har använt saker som std::tie och / eller std::sync_with_stdio för att förhindra det.

Det lämnar bara ett litet antal riktigt ovanliga situationer där du har god anledning att spola en ström manuellt. Sådana fall är sällsynta att det är väl värt att använda std::flush när de händer, för att göra det uppenbart för alla som läser koden att du spolar strömmen medvetet (och oftare än inte, förtjänar förmodligen också en kommentar om varför det här är ett av de sällsynta fall när spolning av strömmen verkligen är vettigt.

Svar

Varje gång en process ger utdata måste den anropa en funktion som faktiskt gör jobbet. I de flesta fall är den funktionen i slutändan write(2). I ett multitasking-operativsystem kommer samtalet till write() att fånga in i kärnan, som måste stoppa processen, hantera I / O, göra andra saker medan några blockeringar är rensade, sätt den i färdig kö och få den igång igen när tiden kommer. Sammantaget kan du ringa all den aktiviteten till systemsamtalskostnader . Om det låter så mycket är det.

Spolning av en buffrad ström * efter att du har skrivit en liten mängd data eller utan att ha någon buffert alls medför att det kostar varje gång du gör det:

 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)  

Så här gjordes de mycket tidiga dagarna tills någon fick reda på att den brände en mycket systemtid. Overheaden kunde hållas nere genom att ackumulera utdata i en buffert tills den var full eller programmet bestämde att det måste skickas omedelbart.(Du kanske vill göra det senare om du sporadiskt producerar utdata som behöver ses eller konsumeras.) Om du undviker en spolning i slutet av varje rad minskar antalet systemanrop och kostnaderna uppstår:

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

* Observera att begreppet standardutdata är en filbeskrivare associerad med en process och ges ett välkänt nummer. Detta skiljer sig från stdout definierad av C, C ++ och andra, vilka är identifierare för implementeringar av en buffrad ström som lever helt i användarlandet och skriver till standardutgång. write() systemanropet är inte buffrat.

Kommentarer

  • Huruvida och när man ska spola beror på i vilket sammanhang den utdata kommer att användas. Ett genomströmningsorienterat program ska bara spola när dess buffertar är fulla. Ett latenskänsligt program ska spola oftare. Till exempel om utdata går till en konsol skulle du sti Spola efter varje ny linje. Interaktiva program (som visar en inmatningsprompt) ska spola omedelbart, även om raden ännu inte är full.
  • ” om utdata går till en konsol, du skulle fortfarande spola efter varje ny rad. ” Det är sant, men om utdata går till en konsol sker det en automatisk spolning efter varje ny rad. Inget behov av att göra det uttryckligen.
  • @amon Du skulle fortfarande vilja samla på varandra följande utdata, eftersom spolning i rätt tid, det vill säga innan du ber om inmatning och utan försenad fördröjning, är tillräckligt. Visserligen är det bättre att spola en gång för ofta än att ha för mycket fördröjning i att visa produktionen, eller investera för mycket arbete i att optimera …
  • Ett trick som ibland används av buffertarna är att starta om / starta en timer när nya saker kommer och spola när timern tar slut.

Svar

Varför spolning ska undvikas:

Eftersom IO fungerar bäst när operativsystemet kan arbeta med relativt stora datamängder. Regelbundna spolningar med små mängder data orsakar avmattningar, ibland väldigt markant.

Varför du nästan aldrig ska spola manuellt:

Det finns automatiska spolningar som täcker de flesta användningsfall. Till exempel, om ett program skriver till konsolen, spolas systemet som standard efter varje ny rad. Eller om du skriver till filen skrivs data när det finns tillräckligt med data som ska skrivas på en gång och även när filen stängs.

När Du bör spola manuellt:

Om du uttryckligen behöver uppdatera utdata omedelbart. Exempel: om du skapar en spinner eller förloppsindikator som upprepade gånger skriver över den aktuella raden. Eller om du matar ut till filen och verkligen vill att filen ska uppdateras vid specifika ögonblick.

Kommentarer

  • Systemet kan inte spola några användarbuffertar. Eller tecknar du bibliotek, särskilt standardbiblioteket, även under ” -system ”? Naturligtvis är stdin och stdout i allmänhet kopplade åtminstone om de båda använder samma konsol.
  • Ja, Jag tyckte inte ’ att det vore en bra idé att gå för mycket i detalj om hur saker delas mellan användarutrymme bibliotek och kärnutrymme.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *