Skip to content

69 Intermediate The Hundred

The Newline That Cost a Syscall

two loops, the same 200 000 lines, one manipulator apart

using namespace std::chrono;

int main() {
    auto t0 = steady_clock::now();
    {
        std::ofstream log("endl_demo.tmp");
        for (int i = 0; i < 200000; ++i)
            log << "event " << i << '\n';
    }
    auto t1 = steady_clock::now();
    {
        std::ofstream log("endl_demo.tmp");
        for (int i = 0; i < 200000; ++i)
            log << "event " << i << std::endl;
    }
    auto t2 = steady_clock::now();
    std::cout << "'\\n':      " << duration_cast<milliseconds>(t1 - t0).count() << " ms\n";
    std::cout << "std::endl: " << duration_cast<milliseconds>(t2 - t1).count() << " ms\n";
    std::remove("endl_demo.tmp");
}

Run it. How far apart are the two timings?

Answer

Not close: the flushing loop takes tens of times longer (GCC 11.5, x86-64 Linux, ext4):

'\n':      38 ms
std::endl: 627 ms

One run of many. The ratio held above 10× on every run here, usually near 16×, -O0-O3 alike — and understated: '\n' goes first, on a file it must create; swap the loops, 26×.

Why

std::endl writes exactly the byte '\n' writes — and then calls flush(). Flushing hands the buffer to the kernel — nothing is fsynced — so the std::endl loop makes one write syscall per line: strace -c counts 200 000. The '\n' loop fills the stream buffer (8 KiB with libstdc++) and flushes only when full: 304 calls for the same 2.5 MB, 303 of them writev — some 650× fewer crossings, and the crossing is the whole cost: on tmpfs, with no disk in the path at all, std::endl still burns 300 ms. -Wall -Wextra says nothing.

The fix

Write the newline, and flush only when something is genuinely waiting to see the output:

log << "event " << i << '\n';      // buffered — the stream flushes when it is ready
std::cout << '.' << std::flush;    // deliberate: a progress dot, wanted on screen now

A stream flushes when its buffer fills and again in its destructor, so only a crash loses the tail — std::endl's one honest use. std::cerr has unitbuf set: it flushes every insertion.

Takeaway: std::endl is a newline plus a flush, and the flush is the expensive half.

Try it: g++ -std=c++17 main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry