找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖
Claude、GPT 海外模型 API 接入Claude skills 从入门到精通 吴恩达亲授 AI Agent 核心技能2026 瞪哥公务员考试全攻略 行测申论一站式系统备考
Agent 文心智能蒸馏模型实战 90G 课程智泊 AI 大模型训练营 基于 LangChain 的 RAG 与提示工程实战构建企业级 AI 大脑:大模型微调与 RAG / Agent 全栈实战

6289

积分

0

好友

791

主题
发表于 昨天 23:03 | 查看: 2| 回复: 0

写 C++ 好多年了,也研究过不少开源代码,发现一个共性:输出换行时,大家基本都用 \n,而不是 std::endl。就像下面这样:

std::cout << "hello\n";

原因很简单,std::endl 不只是输出一个换行符,它还会调用 flush() 刷新输出缓冲区;而 \n 只是输出换行,并不会主动要求刷新。

于是,一个很自然的结论出现了:std::endl 会频繁触发系统调用,而系统调用很昂贵,所以代码中应该尽量换成 \n。

这个结论我一度也觉得没什么问题。不过真去测一下,会发现一个奇怪的现象:把程序里的 std::endl 全部换成 \n,系统调用次数可能一次都没少。

示例

用一段示例代码验证下这个结论。代码很简单,循环输出 30 万行数据,每行 16 字节,总共大约 4.8 MB。

if (use_endl) {
    for (long i = 0; i < 300000; ++i)
        std::cout << PAYLOAD << std::endl;
} else {
    for (long i = 0; i < 300000; ++i)
        std::cout << PAYLOAD << '\n';
}

std::cout.flush();

这里分别测试两种情况:第一种直接输出到终端,第二种把 stdout 重定向到文件。然后通过 strace 统计两种情况下 write() 系统调用的次数。

# 输出到终端
strace -c ./test

# 输出重定向到文件
strace -c ./test > output.txt

按照直觉,结果应该很明显:std::endl 每行都刷新一次,所以会产生大量 write();而 \n 不会主动 Flush,可以先把数据留在 Buffer 里,等 Buffer 满了再批量写入内核。

但实际结果却是:

                输出位置       write 次数
std::endl       terminal        300001
\n              terminal        300001

std::endl       file            300001
\n              file              1173

输出重定向到文件时,一切符合预期:把 std::endl 换成 \n 后,write() 从 30 万次降到了 1173 次。但输出到终端时,两者竟然都是 300001 次。

也就是说,同样的代码、同样的数据,仅仅改变 stdout 的去向,结果就完全不同:

Terminal  →  std::endl 300001 次,\n 300001 次
File      →  std::endl 300001 次,\n   1173 次

同样的代码,仅仅改变 stdout 的输出目标,结果就完全不同。 要解释这个现象,还得继续往下看 std::cout 的输出过程。

内部 Buffer

前面的现象有点奇怪:std::endl 会主动 Flush,\n 不会,但输出到终端时,两者产生的 write() 次数却完全一样。

要解释这个问题,首先得搞清楚一件事:std::cout 输出的数据,在进入内核之前到底缓存在什么地方?

很多人可能会下意识认为,std::cout 自己维护着一块 Buffer,数据先写进这里,等 Buffer 满了或者调用 flush(),再通过 write() 交给内核:

std::cout → C++ Buffer → write() → Kernel

但在 libstdc++ 的默认配置下,实际情况并没有这么简单。默认情况下:

std::ios::sync_with_stdio(true);

也就是 C++ iostream 与 C stdio 保持同步。此时 std::cout 底层使用的是 stdio_sync_filebuf,写入数据最终会走到类似:

std::fwrite(...);

而:

std::cout.flush();

最终会走到类似:

std::fflush(stdout);

所以这时候更接近真实的数据路径其实是:

std::cout → C stdio → stdout Buffer → write() → Kernel

也就是说,我们真正应该关注的 Buffer,不是在 std::cout 这一层,而是在下面的 stdio。

这就能解释前面的怪现象了:既然最终的数据缓冲由 stdio 负责,那么什么时候真正调用 write(),就不只取决于 C++ 有没有调用 flush(),还取决于 stdio 自己采用了什么缓冲策略。

而这个策略,又和 stdout 最终连接的是终端还是文件有关。

终端和文件

在 Linux 上,stdout 本质上就是文件描述符 1,glibc 会根据 stdout 最终连接的对象选择不同的缓冲策略。

如果 stdout 指向普通文件,就像下面这样:

./test > output.txt

通常采用全缓冲(Full Buffering)。数据先进入 Buffer,Buffer 满了以后再调用 write():

程序 → Buffer → Buffer 满 → write() → Kernel

所以大量:

std::cout << "hello\n";

不会每次都触发 write()。示例代码里一次大约写 4096 字节,因此 4.8 MB 数据最终只有 1173 次左右系统调用。

但终端不一样。如果 stdout 指向 Terminal,stdio 通常采用行缓冲(Line Buffering),遇到换行就会把当前行送出去。

这时候最开始的谜团就解开了。

我们一直认为 std::endl 会 Flush,而 \n 不会 Flush。从 C++ 接口语义上说没有问题,但当 stdout 指向终端时,底层行缓冲机制本身就会因为换行而把数据刷新出去。

于是两条路径最终变成:

std::endl → 换行 → 显式 flush → write()
\n        → 换行 → 行缓冲刷新 → write()

所以:

std::endl   terminal   300001
\n          terminal   300001

不是 std::endl 没有 Flush,而是 \n 在这个环境下最终也导致了输出刷新。

差距

现在再看:

./test > output.txt

就很好理解了。stdout 不再指向终端,而是普通文件,stdio 从行缓冲变成全缓冲。于是 \n 只是继续往 Buffer 里写,只有 Buffer 满了才批量调用 write()。

但 std::endl 不一样,因为它明确要求 flush(),所以无论底层是什么缓冲模式,都必须刷新。

最终才出现:

std::endl   file   300001
\n          file     1173

30 万次和 1173 次,相差大约 256 倍。

所以“把 std::endl 换成 \n 可以减少系统调用”这句话不能说错,但它少了一个非常重要的前提:

你的输出最终去了哪里?

sync_with_stdio(false)

很多 C++ 性能优化文章除了告诉你把 std::endl 换成 \n,通常还会顺手加一句:

std::ios::sync_with_stdio(false);

这行代码经常被描述成“关闭 C/C++ IO 同步,可以让 cout 更快”。但如果继续往下看,会发现它改变的远不只是一个同步开关。

执行以后,std::cout 底层从 stdio_sync_filebuf 变成 stdio_filebuf,这时候 C++ Stream 开始使用自己的 Buffer。实验中观察到的 Buffer 大约是 8191 字节,数据路径也从:

std::cout → stdio/glibc Buffer → write()

变成更接近:

std::cout → C++ Stream Buffer → write()/writev()

然后最有意思的事情发生了。重新测试:

                         系统调用次数
endl + terminal             300001
endl + file                 300001

\n + terminal                  587
\n + file                      587

这一次,Terminal 和 File 的区别基本消失了。因为 std::cout 不再依赖 stdout 原来的行缓冲策略,C++ 自己维护 Buffer,于是 \n 终于真的只是写入一个换行符,不会因为终端行缓冲立即刷新。

反而是在执行:

std::ios::sync_with_stdio(false);

以后,std::endl 和 \n 在终端上的性能差距才真正显现出来。

很多时候我们把 sync_with_stdio(false) 和 std::endl → \n 当成两个独立的优化建议,但从底层看,它们并不是完全独立的:前者改变了 Buffer 和 IO 路径,后者的实际性能影响也因此发生了变化。

性能不一定提升

还有一个容易被忽略的问题。关闭同步以后,实验中的系统调用从 1173 次进一步下降到了 587 次,差不多少了一半,但执行时间只是从大约 9.86 ms 下降到 8.11 ms,并没有快一倍。

原因很简单:系统调用次数只是性能的一部分。最终仍然要搬运相同的 4.8 MB 数据,区别只是从“很多次小 write()”变成“更少次数的大 write()/writev()”。

所以:

syscall 减少 50% ≠ 性能提升 50%

结语

回到最开始的问题,std::endl 和 \n 到底哪个快?

如果只回答:

\n 更快,因为 std::endl 会 Flush。

不能说错,但离真正理解这个问题还差了好几层。因为程序真正执行的路径可能是:

C++ Stream → streambuf → C stdio/glibc → Buffering Policy → write/writev → Kernel → Terminal/File/Pipe

任何一层发生变化,最终测出来的结果都可能不同。

输出到 Terminal 时,std::endl 和 \n 的系统调用次数可能一样,因为底层本身就在做行缓冲;重定向到文件后,两者差距可能突然放大,因为 stdout 变成了全缓冲;关闭 sync_with_stdio(false) 后,Terminal 和 File 的差别又可能消失,因为 C++ 开始使用自己的 Buffer。




上一篇:Claude Code vs Codex 怎么选:复杂排查和异步任务该交给谁?
下一篇:GitHub 2.4万电子书索引实测:能搜但不存书,数据成色拆解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-25 03:09 , Processed in 0.583976 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表