Perché vuoi evitare di scaricare lo stdout?

Mi sono imbattuto in una domanda in Codereview e in una risposta il feedback era: evita std::endl perché svuota lo stream. La citazione completa è:

Consiglio “di evitare std::endl in generale. Oltre a scrivere un nuovo- line al flusso, scarica il flusso. Vuoi la nuova linea, ma quasi mai vuoi scaricare il flusso, quindi è generalmente meglio scrivere solo un \ n. Nelle rare occasioni in cui desideri effettivamente il flush, fallo esplicitamente: std::cout << "\n" << std::flush;.

Il poster lo ha fatto Non spiegarlo, né nel post né nei commenti. Quindi la mia domanda è semplicemente questa:

Perché vuoi evitare il flush?

Ciò che mi ha reso ancora più curioso è stato il fatto che il poster dice che è molto raro che tu voglia lavare. Non ho problemi a immaginare situazioni in cui vuoi evitare di arrossire, ma ho comunque pensato che in generale lo avresti voluto flush quando stampi una nuova riga. Dopotutto, non è questo il motivo per cui std::endl sta scaricando in primo luogo?

Solo per commentare in anticipo i voti di chiusura:

Non considero questa opinione basata. Quello che dovresti preferire potrebbe essere basato sullopinione, ma ci sono ragioni oggettive da prendere in considerazione. Le risposte finora lo dimostrano. Il lavaggio influisce sul rendimento.

Commenti

Risposta

La risposta breve e semplice è che luso di std::endl può e rallenterà loutput di un enorme margine. In effetti, sono ragionevolmente convinto che std::endl sia responsabile la maggior parte dellidea che gli iostream C ++ siano sostanzialmente più lenti dellI / O in stile C. / p>

Ad esempio, considera un programma come questo:

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

Con loutput standard reindirizzato a un file, questo produce i seguenti risultati:

 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 

Lutilizzo di std::endl ha rallentato il programma di un fattore di circa 20. In questo caso, se si scrivevano stringhe più brevi, il rallentamento potrebbe / sarebbe anche maggiore.

Ci sono alcuni casi in cui si desidera veramente e veramente scaricare un flusso manualmente, ma onestamente sono piuttosto pochi e rari tra loro.

La maggior parte delle volte uno stream deve essere scaricato (ad esempio, si stampa un prompt, quindi si attende un input) “avverrà automaticamente a meno che non si siano utilizzate cose come std::tie e / o std::sync_with_stdio per evitarlo.

Ciò lascia solo un numero esiguo di situazioni veramente insolite in cui hai buon motivo per scaricare manualmente un flusso. Questi casi sono abbastanza rari che vale la pena usare std::flush quando si verificano, per rendere evidente a chiunque legga il codice che “stai scaricando lo stream intenzionalmente (e più spesso che no, probabilmente merita anche un commento sul perché questo è uno dei rari casi in cui lo svuotamento del flusso ha davvero senso).

Risposta

Ogni volta che un processo produce un output, deve chiamare una funzione che effettivamente fa il lavoro. Nella maggior parte dei casi, tale funzione è in definitiva write(2). Su un sistema operativo multitasking, la chiamata a write() si intrappolerà nel kernel, che deve fermare il processo, gestire lI / O, fare altre cose mentre eventuali blocchi vengono cancellati, mettere metterlo in coda pronto e farlo funzionare di nuovo quando sarà il momento. Collettivamente, puoi chiamare tutta questa attività overhead delle chiamate di sistema . Se suona come un sacco, lo è.

Lo svuotamento di un flusso bufferizzato * dopo aver scritto una piccola quantità di dati o non avendo alcun buffer incorre in quel sovraccarico ogni volta che lo fai:

 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)  

Questo è come è stato fatto nei primissimi giorni fino a quando qualcuno ha capito che stava bruciando un molto tempo di sistema. Loverhead può essere mantenuto accumulando loutput in un buffer finché non è pieno o il programma decide che deve essere inviato immediatamente.(Potresti voler fare il secondo se stai producendo sporadicamente output che deve essere visto o consumato.) Evitare un flush alla fine di ogni riga riduce il numero di chiamate di sistema e il sovraccarico sostenuto:

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

* Nota che il concetto di output standard è un descrittore di file associato a un processo e dato un numero ben noto. Questo differisce dal stdout definito da C, C ++ e altri, che sono identificatori per le implementazioni di uno stream bufferizzato che risiede interamente nello spazio utente e scrive nel output standard. La chiamata di sistema write() non è bufferizzata.

Commenti

  • Se e quando scaricare dipende dal contesto in cui verrà utilizzato loutput. Un programma orientato al throughput dovrebbe svuotare solo quando i suoi buffer sono pieni. Un programma sensibile alla latenza dovrebbe svuotare più spesso. Ad esempio, se loutput va a una console, svuoterà dopo ogni nuova riga. I programmi interattivi (che mostrano un prompt di input) dovrebbero scaricarsi immediatamente, anche se la riga non è ancora piena.
  • ” se loutput va a una console, verrebbe comunque scaricato dopo ogni nuova riga. ” Vero, ma se loutput va a una console, cè uno scaricamento automatico dopo ogni nuova riga. Non cè bisogno di farlo in modo esplicito.
  • @amon Vorresti comunque unire un output consecutivo, poiché lo svuotamento tempestivo, ovvero prima di chiedere input e senza ritardi, è abbastanza sufficiente. Certo, è meglio svuotare una volta di troppo spesso che avere troppo ritardo nel mostrare loutput, o investire troppo lavoro nellottimizzazione …
  • Un trucco a volte usato dai bufferer è riavviare / avviare un timer quando arrivano nuove cose e svuota quando il tempo scade.

Rispondi

Perché è necessario evitare lo scaricamento:

Perché IO funziona meglio quando il sistema operativo può lavorare con quantità di dati relativamente grandi. Scarichi regolari con piccole quantità di dati causano rallentamenti, a volte molto significativi.

Perché non dovresti quasi mai scaricare manualmente:

Ci sono svuotamenti automatici che coprono la maggior parte dei casi duso. Ad esempio, se un programma scrive sulla console, il sistema per impostazione predefinita esegue lo svuotamento dopo ogni nuova riga. Oppure, se scrivi su file, i dati vengono scritti una volta che ci sono abbastanza dati per essere scritti contemporaneamente e anche quando il file viene chiuso.

Quando dovresti svuotare manualmente:

Se hai esplicitamente bisogno di aggiornare immediatamente loutput. Esempio: se crei una casella di selezione o una barra di avanzamento che sovrascrive ripetutamente la riga corrente. O se esegui loutput su file e desideri davvero che il file venga aggiornato in momenti specifici.

Commenti

  • Il sistema non può svuotare alcun buffer utente. Oppure riassumi le librerie, in particolare la libreria standard, anche sotto ” system “? Ovviamente, stdin e stdout sono generalmente accoppiati almeno se utilizzano entrambi la stessa console.
  • Sì, Non ‘ pensavo che sarebbe stata una buona idea entrare troppo nei dettagli su come le cose sono suddivise tra le librerie dello spazio utente e lo spazio del kernel.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *