Méně známé skutečnosti o C++: std::endl vs '\n'

Od Petr Zemek, 2013-06-09

Často se setkávám s tím, že se v C++ k ukončení řádku ve výstupu používá std::endl místo klasického '\n', které známe z Céčka. Důvod bývá různý, ale většinou je to něco ve stylu: "je to C++kovější řešení než '\n'" či "je to přenositelnější". V tomto příspěvku se dozvíte, že ani jeden důvod není pravdivý a že použití '\n' místo std::endl vám může značně urychlit běh programu.

Motivační příklad na začátek

Dnešní příspěvek začnu poněkud netradičně, a to motivačním příkladem. Mějme následující dva kusy kódu:

// (1)
for (size_t i = 0; i < 10000000; ++i) {
    std::cout << "Here comes a string..." << std::endl;
}
// (2)
for (size_t i = 0; i < 10000000; ++i) {
    std::cout << "Here comes a string..." << '\n';
}

Když oba dva kódy přeložíme a spustíme, dostaneme identický výstup. Co je ale zajímavé, tak je čas běhu jednotlivých řešení, uvedený níže.

// (1)
real    1m3.699s
user    0m4.030s
sys     0m34.203s
// (2)
real    0m2.066s
user    0m0.810s
sys     0m0.687s

Celkem síla, že? Všechny testy proběhly na systému s 64b Arch Linux, kernel 3.9.3, Intel Core 2 Duo T8300 2.4 GHz, 4 GB RAM, 320 GB SATA WDC WD3200BEVT-2 (5400rpm). Použitá verze gcc je 4.8.0 a libstdc++5 je ve verzi 3.3.6-4. Použité parametry při překladu byly -std=c++11 -pedantic -O2. Testy proběhly pětkrát za sebou a jejich čas byl zprůměrován. Výstup byl přesměrován do souboru.

Vysvětlení

Důvod, proč je první řešení tak pomalé, je v tom, že std::endl plní (oproti '\n') dvě funkce: první je odřádkování a druhá je vyprázdnění vyrovnávací paměti [27.7.3.8 v ISO C++11]. Standardní proud std::cout je implicitně tzv. bufferovaný, což znamená, že je využita vyrovnávací paměť. K zápisu na výstup (či disk) pak dojde až ve chvíli, kdy je vyrovnávací paměť plná nebo explicitně vyprázdněná (v C++ k tomu lze využít standardní manipulátor std::flush, ale implicitně to udělá i std::endl). Co je třeba si uvědomit, tak je, že při každém vyprázdnění vyrovnávací paměti je třeba proces uspat, přejít z uživatelského režimu do režimu jádra, provést zápis na disk (nejpomalejší část z celé operace) a vrátit se zpět do uživatelského režimu. Pokud použijeme '\n', tak se tato činnost provede až tehdy, když se vyrovnávací paměť naplní. Tento rozdíl je názorně vidět u položky sys výše, která značí, kolik času bylo stráveno v jádru.

Jak je to tedy s tím rozdílem mezi std::endl a '\n'?

Jediný rozdíl je v tom, že std::endl oproti '\n' vždy provede vyprázdnění vyrovnávací paměti. Použití tohoto manipulátoru nemá žádný vliv na přenositelnost programu, ani se nejedná o C++kovější řešení, než '\n'.

Co tedy používat?

Pokud používáte ladicí výpisy či chcete mít výstup okamžitě k dispozici (např. do logu), tak std::endl, jinak '\n'. V případě ladicích výpisů ale zvažte spíše použití std::cerr, který je implicitně nebufferovaný (tj. bez vyrovnávací paměti) a není tak třeba používat std::endl ani std::flush. Můžete pak vždy používat '\n', což může být rychlejší, viz náš motivační příklad; obzvlášť, pokud výpis provádíte frekventovaně a ve velkém množství (u prográmků ve stylu "Ahoj světe" se to neprojeví). Co je typicky naprostý nesmysl, který však běžně vídávám, tak je

std::cerr << "error: xxx" << std::endl; // Takto ne...

Ono volání std::endl je zbytečné (std:cerr není bufferovaný). Ideální je pak volat

std::cerr << "error: xxx\n";
Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
2 + 0 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.

Kuba (neověřeno)

11 years 5 months zpět

Ahoj Petře,

s jakou úrovní optimalizace jsi to zkoušel?

Kuba

Petr Zemek

11 years 5 months zpět

In reply to by Kuba (neověřeno)

Vidíš, to jsem tam úplně zapomněl zmínit. Všechno jsem testoval s -O2 (doplnil jsem to tam).

Ota (neověřeno)

11 years 5 months zpět

Ahoj Petře,
myslel jsem, že std::endl se dává kvůli různému chápání konce řádku, např. CR LF.
Jak pozná, že chci na konci řádku dva znaky místo jednoho?

Ota

Petr Zemek

11 years 5 months zpět

In reply to by Ota (neověřeno)

Díky za dotaz. Jak jsem již nastínil v článku, tak

stream << std::endl;

je ekvivalentní s

stream << '\n' << std::flush;

Nyní záleží na tom, co je stream zač. Pokud se jedná textový výstup (cout, soubor otevřený v textovém režimu apod.), tak systém automaticky převede '\n' na ukončovač řádku na dané platformě. Pokud tedy tento kód spustíš na MS Windows, tak se '\n' zapíše jako "\r\n". Pokud to spustíš na Linuxu, tak bude výsledkem '\n'. Pokud je to stream otevřený v binárním režimu, tak se zapíše pouze '\n' (bez konverze).

Suma sumárum, použití std::endl nezvyšuje přenositelnost, protože použitím '\n' dosáhneš stejného efektu, čili pro streamy otevřené v textovém režimu se automaticky zapíše konec řádku dle zvyklostí na dané platformě. Příklad: pokud spustíš následující kód na MS Windows

std::cout << std::endl;
std::cout << '\n';

tak bude výstupem "\r\n\r\n". Pokud jej spustíš na Linuxu, tak bude výstupem "\n\n". To, že použití std::endl zvyšuje přenositelnost, je mýtus. Viz sekce 27.7.3.8 v ISO C++11 normě, kde je detailně specifikované, co std::endl dělá.