De ce doriți să evitați spălarea stdout?

Am dat peste o întrebare în Codereview și, într-un singur răspuns, feedback-ul a fost să evitați std::endl deoarece spală fluxul. Citatul complet este:

Aș recomanda evitarea std::endl în general. Împreună cu scrierea unui nou linie către flux, curăță fluxul. Doriți noua linie, dar aproape niciodată nu doriți să curățați fluxul, deci este, în general, mai bine să scrieți doar un \ n. În rara ocazie în care doriți efectiv spălarea, faceți-o în mod explicit: std::cout << "\n" << std::flush;.

nu explica acest lucru, nici în postare sau comentarii. Întrebarea mea este pur și simplu următoarea:

De ce doriți să evitați spălarea?

Ceea ce m-a făcut și mai curios a fost că afișul spune că „este foarte rar că vrei să speli. Nu am nicio problemă să-mi imaginez situații în care vrei să eviți spălarea, dar totuși am crezut că tu, în general, ai vrea să spălați când imprimați o linie nouă. La urma urmei, nu este „motivul pentru care std::endl se spală în primul rând?

Doar pentru a comenta în avans voturile apropiate:

Nu consider această opinie bazată. Ceea ce ar trebui să preferați poate fi bazat pe opinie, dar există motive obiective de luat în considerare. Răspunsurile de până acum demonstrează acest lucru. Curățarea afectează performanța.

Comentarii

Răspuns

Răspunsul scurt și simplu este că utilizarea std::endl poate și va încetini ieșirea cu o marjă imensă. De fapt, sunt „convins în mod rezonabil că std::endl este responsabil pentru majoritatea noțiunii că fluxurile i ++ C ++ sunt substanțial mai lente decât I / O în stil C.

De exemplu, luați în considerare un program de acest gen:

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

Cu ieșirea standard redirecționată către un fișier, acesta produce următoarele rezultate:

 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 

Utilizarea std::endl a încetinit programul cu un factor de aproximativ 20 în acest caz. Dacă ați scris șiruri mai scurte, încetinirea ar putea / ar fi și mai mare.

Există câteva cazuri în care doriți cu adevărat să spălați manual un flux – dar, sincer, sunt destul de puțini.

De cele mai multe ori un flux trebuie spălat (de exemplu, tipăriți o solicitare, apoi așteptați o intrare) se va întâmpla automat, cu excepția cazului în care ați folosit lucruri precum std::tie / sau std::sync_with_stdio pentru a preveni acest lucru.

Acest lucru lasă doar un număr mic de situații cu adevărat neobișnuite în care aveți un motiv bun pentru a spăla un flux manual. Astfel de cazuri sunt destul de rare încât „merită să utilizați std::flush atunci când se întâmplă, pentru a face evident pentru oricine citește codul că„ spălați fluxul intenționat (și mai des decât nu, probabil merită și un comentariu despre de ce acesta este unul dintre cazurile rare când spălarea fluxului are cu adevărat sens).

Răspuns

De fiecare dată când un proces produce ieșire, trebuie să apeleze o funcție care efectiv funcționează. În majoritatea cazurilor, această funcție este în cele din urmă write(2). Pe un sistem de operare multitasking, apelul către write() va fi capturat în nucleu, care trebuie să oprească procesul, să gestioneze I / O, să facă alte lucruri în timp ce blocajele sunt eliminate, pe coada gata și puneți-l din nou în funcțiune când vine momentul. În mod colectiv, puteți apela toată acea activitate apel apel sistem overhead . Dacă sună foarte mult, este așa.

Spălarea unui flux tamponat * după ce ați scris o cantitate mică de date sau nu ați avut deloc buffer care implică costuri suplimentare de fiecare dată când faceți acest lucru:

 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)  

Așa s-a făcut în primele zile până când cineva și-a dat seama că arde o mult timp de sistem. Cheltuielile generale ar putea fi menținute în jos prin acumularea de ieșiri într-un buffer până când a fost plin sau programul a decis că trebuie trimis imediat.(S-ar putea să doriți să faceți acest lucru din urmă dacă „produceți sporadic ieșiri care trebuie văzute sau consumate.) Evitarea unei flush-uri la sfârșitul fiecărei linii reduce numărul de apeluri de sistem și cheltuielile generale suportate:

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

* Rețineți că conceptul de ieșire standard este un descriptor de fișiere asociat cu un proces și dat un număr bine cunoscut. Acest lucru diferă de stdout definit de C, C ++ și alții, care sunt identificatori pentru implementările unui flux tamponat care trăiesc în întregime în userland și scriu în ieșire standard. Apelul de sistem write() nu este tamponat.

Comentarii

  • Dacă și când să spălați depinde de contextul în care va fi utilizată acea ieșire. Un program orientat spre transfer ar trebui să se spargă numai atunci când bufferele sale sunt pline. Un program sensibil la latență ar trebui să se spele mai des. Voi spăla după fiecare linie nouă. Programele interactive (care afișează unele solicitări de intrare) ar trebui să fie spălate imediat, chiar dacă linia nu este încă plină.
  • ” dacă ieșirea merge la o consolă, ar continua să curgă după fiecare linie nouă. ” Adevărat, dar dacă ieșirea merge la o consolă, există o culoare automată după fiecare linie nouă. Nu este nevoie să faceți acest lucru în mod explicit.
  • @amon Ați dori totuși să reuniți ieșirea consecutivă, deoarece spălarea în timp util, adică înainte de a solicita intrarea și fără întârziere, este suficientă. Desigur, este mai bine să spălați o dată prea des decât să aveți prea multă întârziere în afișarea rezultatului sau să investiți prea multă muncă în optimizare …
  • Un truc folosit uneori de tampoane este să re / porniți un cronometru când apar lucruri noi și spălați când cronometrul se termină.

Răspuns

De ce trebuie evitată spălarea:

Deoarece IO funcționează cel mai bine atunci când sistemul de operare poate funcționa cu cantități relativ mari de date. Curățările obișnuite cu cantități mici de date provoacă încetiniri, uneori foarte semnificative.

De ce nu ar trebui să curățați aproape niciodată manual:

Există flush-uri automate care acoperă cele mai multe cazuri de utilizare. De exemplu, dacă un program scrie pe consolă, sistemul implicit se spală după fiecare linie nouă. Sau dacă scrieți în fișier, datele sunt scrise odată ce există suficiente date pentru a fi scrise simultan și, de asemenea, când fișierul este închis.

Când ar trebui să spălați manual:

Dacă trebuie să actualizați imediat rezultatul imediat. Exemplu: dacă creați un spinner sau o bară de progres care suprascrie în mod repetat linia curentă. Sau dacă ieșiți într-un fișier și doriți cu adevărat ca fișierul să fie actualizat în anumite momente.

Comentarii

  • Sistemul nu poate spăla niciun tampon de utilizator. Sau includeți biblioteci, în special biblioteca standard, și sub ” sistem „? Desigur, stdin și stdout sunt în general cuplate cel puțin dacă ambele folosesc aceeași consolă.
  • Da, ‘ nu m-am gândit că ar fi o idee bună să intru în prea multe detalii despre modul în care lucrurile sunt împărțite între bibliotecile spațiului utilizator și spațiul kernel. div>

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *