Miért akarja elkerülni a stdout kipirulását?

Megtaláltam egy kérdést a Codereview alkalmazásban, és az egyik válasz a következő volt: kerülje az std::endl alkalmazást, mert az átöblíti a folyamot. A teljes idézet:

Azt tanácsolom, hogy általában kerülje a std::endl t. Egy új- A patakhoz vezetõ vonal öblíti a folyamot. Szeretné az új sort, de szinte soha nem akarja öblíteni a folyamot, ezért általában jobb, ha csak egy \ n-t ír. Azon ritka esetben, amikor valóban szeretné az öblítést, tegye meg kifejezetten: std::cout << "\n" << std::flush;.

A poszter ezt ne magyarázd el, sem a bejegyzésben, sem a kommentekben. Tehát a kérdésem egyszerűen ez:

Miért akarja elkerülni a kipirulást?

Ami még kíváncsibbá tett, hogy a poszter szerint nagyon ritka, hogy le akarsz öblíteni. Nincs gondom elképzelni azokat a helyzeteket, amikor el akarod kerülni a kipirulást, de mégis úgy gondoltam, hogy általában vennéd öblítse ki, amikor új sort nyomtat. Végül is nem ez az oka annak, hogy a std::endl miért elsõsorban elpirul?

Csak a zárószavazások előzetes kommentálása érdekében:

Ezt a véleményt nem tartom alapul. Lehet, hogy véleményalapú, de ezt objektív okokkal kell figyelembe venni. Az eddigi válaszok ezt bizonyítják. Az öblítés befolyásolja a teljesítményt.

Megjegyzések

Válasz

A rövid és egyszerű válasz az, hogy az std::endl használata hatalmas különbséggel lassíthatja és lassíthatja a kimenetet. Valójában “megalapozottan meg vagyok győződve arról, hogy std::endl felelős a legtöbb értelemben, hogy a C ++ iostreamek lényegesen lassabbak, mint a C-stílusú I / O-k. / p>

Vegyünk például egy ilyen programot:

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

A standard kimenet fájlba átirányításával a következő eredmények születnek:

 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 

A std::endl használata ebben az esetben körülbelül 20-szorosára lassította a programot. Ha rövidebb karakterláncokat írt, a lassítás még nagyobb lehet / lenne.

Van néhány olyan eset, amikor valóban és valóban szeretné manuálisan átmosni a folyamot – de őszintén szólva ezek elég kevesen vannak.

A legtöbbször egy adatfolyamot ki kell öblíteni (pl. kinyomtat egy üzenetet, majd megvárja a bevitelt). Ez automatikusan megtörténik, hacsak nem használtál olyan dolgokat, mint std::tie és / vagy std::sync_with_stdio ennek megakadályozására.

Ez csak csekély számú valóban szokatlan helyzetet hagy maga után jó ok a folyam manuális átmosására. Ilyen esetek elég ritkák, így érdemes megtenni a std::flush t, amikor történnek, hogy a kódot olvasó bárki számára nyilvánvalóvá váljon, hogy szándékosan (és gyakrabban) öblíted le az adatfolyamot mint nem, valószínűleg megemlít egy megjegyzést arról is, hogy miért ez az egyik ritka eset, amikor a patak öblítésének valóban van értelme).

Válasz

Valahányszor egy folyamat kimenetet produkál, meg kell hívnia egy olyan funkciót, amely valóban elvégzi a munkát. A legtöbb esetben ez a függvény végül write(2). Többfeladatos operációs rendszeren a write() hívás csapdába esik a kernelben, amelynek le kell állítania a folyamatot, kezelnie kell az I / O-t, más dolgokat kell végrehajtania, miközben az esetleges blokkolások megszűnnek, készenléti sorban, és amikor újra eljön az ideje, indítsa újra. Összességében az összes tevékenységet hívhatja rendszerhívás rezsiként . Ha ez soknak tűnik, akkor az.

A pufferelt adatfolyam * átöblítése, miután kis mennyiségű adatot írt, vagy egyáltalán nincs puffer, minden egyes művelet során felmerül a rezsivel:

 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)  

Így tették a legelején, amíg valaki rájött, hogy ég sok rendszeridő. A rezsicsökkentést úgy tehetjük meg, hogy a kimenetet egy pufferben halmozzuk fel, amíg meg nem telik, vagy a program úgy dönt, hogy azonnal el kell küldeni.(Érdemes ez utóbbit megtenni, ha szórványosan hoz létre olyan kimenetet, amelyet látni vagy el kell fogyasztani.) Az egyes vonalak végén lévő süllyesztés elkerülése csökkenti a rendszerhívások számát és a felmerülő rezsit:

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

* Ne feledje, hogy a standard kimenet fogalma egy fájlhoz társított folyamatleíró és jól ismert számot kapott. Ez eltér a C, C ++ és mások által definiált stdout től, amelyek azonosítók a teljes egészében felhasználói országban élő, a szabványos kimenet. A write() rendszerhívás nincs pufferelve.

Megjegyzések

  • Öblítés ideje és ideje attól a kontextustól függ, amelyben az adott kimenetet használni fogják. Az áteresztés-orientált programnak csak akkor kell elmosódnia, ha a pufferei megteltek. A késésérzékeny programnak gyakrabban kell elmosódnia. Pl. Minden új vonal után megöblítem. Az interaktív programoknak (amelyek valamilyen beviteli parancsot mutatnak) azonnal ki kell villanniuk, még akkor is, ha a sor még nincs tele.
  • ” ha a kimenet konzolra megy, akkor minden egyes újsor után még mindig elpirulna. ” Igaz, de ha a kimenet konzolra kerül, akkor minden új sor után automatikus öblítés történik. Ezt nem kell kifejezetten megtenni.
  • @amon Akkor is szeretné összefogni az egymást követő kimenetet, mivel elegendő az időbeli öblítés, vagyis a bemenet kérése előtt és késedelem nélkül. Kétségtelen, hogy jobb, ha túl gyakran öblítünk, mint túl késleltetjük a kimenet megjelenítését, vagy túl sok munkát fektetünk az optimalizálásba …
  • A pufferek néha használják az egyik trükköt, hogy újra / elindítsanak egy időzítő, amikor új dolgok érkeznek, és öblítse le, amikor az időzítő elfogy.

Válasz

Miért kerülendő az öblítés:

Mivel az IO akkor működik a legjobban, ha az operációs rendszer viszonylag nagy mennyiségű adattal képes működni. A kis mennyiségű adattal történő rendszeres öblítés lassulást okoz, néha nagyon jelentősen.

Miért nem szabad szinte soha kézzel öblítenie:

Vannak automatikus öblítések, amelyek lefedik a legtöbb felhasználási esetet. Például, ha egy program ír a konzolra, a rendszer alapértelmezés szerint minden új sor után villog. Vagy ha fájlba ír, akkor az adatokat akkor írja be, ha elegendő adat van egyszerre írható, és a fájl bezárásakor is.

Amikor kézzel kell öblíteni:

Ha kifejezetten azonnal frissítenie kell a kimenetet. Példa: ha olyan tárcsa vagy haladási sávot hoz létre, amely többször felülírja az aktuális sort. Vagy ha fájlba bocsátja ki, és valóban azt szeretné, hogy a fájl adott pillanatban frissüljön.

Megjegyzések

  • A rendszer nem képes öblíteni egyetlen felhasználói puffert sem. Vagy a ” system ” alá sorolja a könyvtárakat, különösen a szabványos könyvtárat? Természetesen a stdin és a stdout általában összekapcsolódnak, legalábbis ha mindkettő ugyanazt a konzolt használja.
  • Igen, Nem gondoltam, hogy ‘ nem lenne jó ötlet túlságosan részletesen kitérni arra, hogy a dolgok hogyan oszlanak meg a felhasználói tér könyvtárai és a kerneltér között.

Vélemény, hozzászólás?

Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük