竞赛模板里那两行std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr);几乎所有人都在抄,但少有人说得清它们各自关掉了什么、代价是什么。更常见的情况是:抄了这两行,代码里还留着printf,然后调试输出开始乱序,因为关掉同步之后,printf和cout就是两条互不知情的缓冲通路。这篇先讲清同步机制,再给出输入的实测加速比和三种格式化方式的对撞结果,最后把「什么时候用哪个」定成一张表。
1. 引子:那两行魔法代码
// 片段:竞赛模板的开头两行,顺序不能反std::ios_base::sync_with_stdio(false);// 断开 iostream 与 C stdio 的绑定std::cin.tie(nullptr);// 解除 cin 对 cout 的自动 flush第一行在默认状态下是true,也就是同步模式:std::cout的每次输出都会和 C 的stdout(printf用的那个)保持一致,std::cin的每次读取也要和stdin保持一致。这种一致性是有代价的:代价具体是什么,看下一节的通路图就清楚了。
2. 同步到底同步了什么
默认(sync_with_stdio(true)):iostream 与 C stdio 走同一条通路 cout << "x" --> cout 的缓冲区 --> stdio_sync_filebuf --> stdout(FILE*) --> fd 1 ^ cin >> v <-- 每次取字符都要经过 stdio 的 fgetc <------+ (不敢自己往前多读, 因为 scanf 也在用) 关闭同步(sync_with_stdio(false))之后:两条完全独立的通路 cout << "x" --> 独立的 filebuf ----------------------------------> fd 1 cin >> v <-- 独立的 filebuf(一次 read 拿一大块,直接整块缓冲)<- fd 1 printf("x") --> stdout(FILE*) 自己的缓冲区 ----------------------> fd 1 ^ 这两条路互不知情:各自的缓冲区各自刷,谁先到 fd 1 就排前面三个要点:
- 默认同步时,
cin不能自己整块预读,因为stdin那头可能还有scanf在用,谁来读、读到哪都必须一致。libstdc++ 的做法是让cin的缓冲区逐个字符去问 stdio(每次都是fgetc一层的函数调用),这就是输入慢的根源。 - 输出侧
cout走的是同一个FILE*,所以「同步」给输出带来的额外开销要小得多。后面实测会看到,输出侧开关同步的差异基本落在噪声里。这和「关同步让输出也快很多」的流行说法不符,值得单独记一笔。 - 一旦关掉同步,混用
printf和cout的行为就不再被保证:两条缓冲区互不知情,输出顺序由各自的刷新时机决定(见第 5 节)。
官方文档:std::ios_base::sync_with_stdio
3. 实测:关同步在输入侧能快多少
基准写成两个独立程序,一个保持默认同步、一个关掉同步,都是「用cin读 20 万个整数」。为了可重复,输入从一个临时文件喂进去(跑完就删),不依赖终端:
// sync_on.cpp — 编译: g++ -std=c++17 -Wall -O2 sync_on.cpp -o demo#include<chrono>#include<cstdio>#include<fstream>#include<iostream>constexprconstchar*kInputPath="miao_tmp_sync_input.txt";constexprintkCount=200000;intmain(){{std::ofstreamout(kInputPath,std::ios::out|std::ios::trunc);for(inti=0;i<kCount;++i){out<<i<<' ';}}if(std::freopen(kInputPath,"r",stdin)==nullptr){return1;}longlongsum=0;constautot0=std::chrono::steady_clock::now();for(inti=0;i<kCount;++i){intv=0;std::cin>>v;sum+=v;}constautot1=std::chrono::steady_clock::now();std::cout<<"sync ON input : ~~ ms (sum="<<sum<<")\n";std::remove(kInputPath);}sync ON input : ~~ ms (sum=19999900000)// sync_off.cpp — 编译: g++ -std=c++17 -Wall -O2 sync_off.cpp -o demo#include<chrono>#include<cstdio>#include<fstream>#include<iostream>constexprconstchar*kInputPath="miao_tmp_sync_input.txt";constexprintkCount=200000;intmain(){std::ios_base::sync_with_stdio(false);// 必须在任何 I/O 之前调用std::cin.tie(nullptr);{std::ofstreamout(kInputPath,std::ios::out|std::ios::trunc);for(inti=0;i<kCount;++i){out<<i<<' ';}}if(std::freopen(kInputPath,"r",stdin)==nullptr){return1;}longlongsum=0;constautot0=std::chrono::steady_clock::now();for(inti=0;i<kCount;++i){intv=0;std::cin>>v;sum+=v;}constautot1=std::chrono::steady_clock::now();std::cout<<"sync OFF input : ~~ ms (sum="<<sum<<")\n";std::remove(kInputPath);}sync OFF input : ~~ ms (sum=19999900000)sum是校验值(0 + 1 + ... + 199999),两次跑出来必须一致,这样即使耗时行用了~~占位,也能确认两次真的读了同样的数据。多次运行的结论:关同步后cin快 3~6 倍。原因就是第 2 节说的:默认模式下每取一个字符都要过一遍 stdio 的fgetc,关掉之后cin可以直接对 fd 做整块read。
| 场景 | 关同步的收益 | 为什么 |
|---|---|---|
cin大量格式化输入 | 3~6 倍(这里实测) | 从「逐字符经 stdio」变成「整块缓冲」 |
cout大量输出 | 差异落在噪声内 | 两者本来就都写到同一个FILE*/fd,缓冲机制相同 |
endl频繁刷缓冲 | 与同步无关,是另一个数量级的问题 | 每次都触发write系统调用(Core Guidelines SL.io.50) |
混用printf/cout | 变成不确定行为 | 两条独立缓冲区,刷新顺序无保证 |
官方文档:std::basic_istream(
sync_with_stdio的效果在std::ios_base页有完整说明)
4. cin.tie(nullptr):断开「读之前先刷输出」
tie()是「绑定的输出流」:在从当前流读取之前,会先把绑定的那个流 flush 一遍。默认std::cin绑定的是std::cout,所以每次cin >> x之前都会 flush 一次cout。
// tie_demo.cpp — 编译: g++ -std=c++17 -Wall -O2 tie_demo.cpp -o demo#include<iostream>intmain(){std::cout<<std::boolalpha;std::cout<<"cin.tie() 默认指向 cout : "<<(std::cin.tie()==&std::cout)<<'\n';std::cout<<"cout.tie() 默认为空 : "<<(std::cout.tie()==nullptr)<<'\n';std::cin.tie(nullptr);std::cout<<"置空后 cin.tie() 为空 : "<<(std::cin.tie()==nullptr)<<'\n';}cin.tie() 默认指向 cout : true cout.tie() 默认为空 : true 置空后 cin.tie() 为空 : true这个默认绑定是为了交互式程序:先cout << "请输入:";再cin >> x,如果没有 tie,提示语可能还躺在缓冲区里没刷出来,用户对着空白终端发呆。它的代价是「每次读都刷一次输出」,在大量读写的场景里就是白刷。所以:
- 交互式程序(命令行工具、按提示输入):保留 tie,提示语必须立刻可见。
- 纯数据处理 / 竞赛:
cin.tie(nullptr)关掉,正确性不受影响。
注意sync_with_stdio(false)和tie(nullptr)是两件独立的事:前者管「iostream 与 C stdio 是否共用缓冲」,后者管「读之前要不要刷某个输出流」。经常被一起抄,但缺一个都不影响另一个的语义。
官方文档:std::basic_ios::tie
5. 混用陷阱:关掉同步后 printf 和 cout 会「各说各话」
这是最容易被忽视的后果。看这个程序(反例,不要这么写:关掉同步后又混用两种输出,违反了sync_with_stdio的前置条件):
// mixed_output.cpp — 反例,不要这么写:关同步后混用 printf 和 cout// 编译: g++ -std=c++17 -Wall -O2 mixed_output.cpp -o demo#include<cstdio>#include<iostream>intmain(){std::ios_base::sync_with_stdio(false);std::printf("printf: 1\n");std::cout<<"cout: 2\n";std::printf("printf: 3\n");std::cout<<"cout: 4\n";}cout: 2 cout: 4 printf: 1 printf: 3代码里明明是「printf、cout、printf、cout」交替写,输出却是四条cout一起出现在前、两条printf挤在后面。原因很直接:cout的缓冲区在静态析构时被刷,C 的stdout缓冲区由exit的 stdio 清理阶段刷。两条独立的缓冲区各自攒、各自刷,顺序只取决于各自什么时候满、什么时候被刷。这里printf的内容少(没攒满一个缓冲区),所以一直等到进程退出才落盘。
这个输出是 gcc-13.2.0 + glibc、stdout 重定向到管道时实测到的,换成终端(行缓冲)或别的标准库实现会得到别的排列,“不确定”本身就是问题的全部。要么全用 iostream,要么全用printf,要么在切换时手动刷:
| 做法 | 结果 |
|---|---|
| 保持默认同步 + 混用 | 顺序正确,但每次输出都有同步开销 |
| 关同步 + 混用 | 顺序不确定(这里实测到乱序) |
| 关同步 + 只用 iostream | 正确且快,推荐 |
| 关同步 + 只用 printf | 正确且快(但要注意cin/cout一处都别碰) |
关同步 + 混用 + 每次cout.flush(); fflush(stdout); | 顺序可控,但把同步开销又加了回来,等于白关 |
官方文档:std::ios_base::sync_with_stdio 的说明与前置条件
6. 三种格式化方式对撞:printf 家族 vs iostream vs std::format(C++20)
最后把「格式化」这一步单独拎出来测:三种写法都只做格式化、不做 I/O(snprintf写进栈缓冲、ostringstream攒在内存、std::format返回std::string),这样测的就是纯粹的格式转换开销。
// verify: std=c++20// format_bench.cpp — 需要 C++20。编译: g++ -std=c++20 -Wall -O2 format_bench.cpp -o demo#include<chrono>#include<cstddef>#include<cstdio>#include<format>#include<iostream>#include<sstream>#include<string>constexprintkRounds=200000;intmain(){usingClock=std::chrono::steady_clock;constautoms=[](Clock::time_point a,Clock::time_point b){returnstd::chrono::duration<double,std::milli>(b-a).count();};std::size_t len=0;charbuf[64]={};constautot0=Clock::now();for(inti=0;i<kRounds;++i){constintn=std::snprintf(buf,sizeofbuf,"%d,%d\n",i,i*2);len+=static_cast<std::size_t>(n);}constautot1=Clock::now();for(inti=0;i<kRounds;++i){std::ostringstream oss;oss<<i<<','<<i*2<<'\n';len+=oss.str().size();}constautot2=Clock::now();for(inti=0;i<kRounds;++i){len+=std::format("{},{}\n",i,i*2).size();}constautot3=Clock::now();std::cout<<"snprintf : ~~ ms\n";std::cout<<"ostringstream : ~~ ms\n";std::cout<<"std::format : ~~ ms\n";std::cout<<"len="<<len<<'\n';}snprintf : ~~ ms ostringstream : ~~ ms std::format : ~~ ms len=7900005len=7900005是三次循环的累计长度校验值,每一次运行都必须相同。耗时的量级关系很稳定:std::format与snprintf同一档,ostringstream慢 1.5~2 倍。为什么ostringstream慢:每一次<<都是一个虚函数调用xsputn,整数转换还要查 locale 的num_putfacet;而snprintf和std::format都是直接走整数到十进制字符的转换例程。
三种方式的能力对比:
| 维度 | printf/snprintf | iostream | std::format(C++20) |
|---|---|---|---|
| 类型安全 | 否:%d传了double是 UB,编译期不报错 | 是 | 是 |
| 格式串检查时机 | 运行时(clang 的-Wformat能补一部分) | 不适用 | 编译期,格式串错直接编译失败 |
| 格式化表达能力 | %+08.2f这类紧凑写法,但无位置索引 | 靠操纵符拼,最长最啰嗦 | {0:>8.2f},还能引用同一个参数多次 |
| 自定义类型 | 不支持 | 重载operator<<即可 | 特化std::formatter |
| 性能 | 快(基准) | 慢 1.5~2 倍 | 与printf同档 |
| 可读性 | 中:参数顺序与格式串容易对不上 | 差:<<链散成一片 | 好:格式串集中、参数紧跟 |
// verify: std=c++20// format_demo.cpp — 需要 C++20。编译: g++ -std=c++20 -Wall -O2 format_demo.cpp -o demo#include<format>#include<iostream>#include<string>intmain(){conststd::string name="miao";constexprdoublekScore=95.5;std::cout<<std::format("{} 的分数是 {:.1f}\n",name,kScore);std::cout<<std::format("[{:>8}][{:<8}][{:^8}]\n","abc","abc","abc");std::cout<<std::format("{:08.3f}\n",3.14159);std::cout<<std::format("{:#x} {:#b} {:#o}\n",255,5,8);std::cout<<std::format("{} {}\n",42,true);std::cout<<std::format("{0} {0} {1}\n","x","y");}miao 的分数是 95.5 [ abc][abc ][ abc ] 0003.142 0xff 0b101 010 42 true x x y几个值得记住的写法:{:>8}右对齐、{:<8}左对齐、{:^8}居中,和printf的%8s/%-8s对得上;{:08.3f}的0是「用 0 填充」;{:#x}带上0x前缀;{}直接打印bool会得到true/false(不用像 iostream 那样先开boolalpha);最后一行{0} {0} {1}是位置索引,同一个参数可以复用:这是printf做不到的。注意 gcc 13 里%这个百分数格式类型还不支持,要自己乘 100。
官方文档:std::format、格式化语法
7. 务实选型
| 场景 | 建议 | 理由 |
|---|---|---|
| 竞赛 / 大规模数据处理 | sync_with_stdio(false)+cin.tie(nullptr),全程只用 iostream | 输入侧 3~6 倍收益,且避开混用陷阱 |
| 现代 C++ 工程(能上 C++20) | std::format拼字符串,再cout << | 编译期格式检查 + 性能与printf同档 |
| 还在 C++17 的工程 | std::format的替代是 {fmt} 库(std::format的雏形就是它) | 同样的格式串语法,API 与std::format高度一致,迁移成本低 |
| 日志 | 用专用日志库(spdlog 之类),别自己在热路径上拼 | 日志库会做异步写、批量刷、格式化缓存 |
| 交互式命令行工具 | 保留cin.tie(),只关同步 | 提示语必须立刻可见 |
| C API / 嵌入式 / 资源受限 | printf家族 | 体积小、无异常、无 locale 依赖 |
一句话:关同步是输入优化,std::format是表达能力和安全性的升级,endl换成'\n'才是纯收益的性能优化。
8. 完整示例:一份日志统计报表
把「关同步 + 切断 tie +std::format对齐输出」拼成一个能整体跑的程序。注意这里全程只用 iostream,这正是关闭同步的安全前提:
// verify: std=c++20// report.cpp — 需要 C++20。编译: g++ -std=c++20 -Wall -O2 report.cpp -o demo#include<format>#include<iostream>#include<string>#include<vector>structRow{std::string level;intcount=0;doubleratio=0.0;};intmain(){std::ios_base::sync_with_stdio(false);// 全程只用 iostream,关同步是安全的std::cin.tie(nullptr);// 非交互程序,不需要读前刷输出conststd::vector<Row>rows{{"INFO",120,0.8},{"WARN",24,0.16},{"ERROR",6,0.04},};std::cout<<std::format("{:<8}{:>8}{:>10}\n","level","count","ratio");std::cout<<std::format("{:-<26}\n","");for(constRow&r:rows){std::cout<<std::format("{:<8}{:>8}{:>8.1f}%\n",r.level,r.count,r.ratio*100.0);}std::cout<<std::format("{:<8}{:>8}{:>8.1f}%\n","total",150,100.0);}level count ratio -------------------------- INFO 120 80.0% WARN 24 16.0% ERROR 6 4.0% total 150 100.0%三件事同时做到了:宽度用{:<8}/{:>8}声明而不是一堆操纵符;{:-<26}用-当填充字符画分隔线(比手写std::string(26, '-')少一次临时字符串构造);关闭同步后所有输出仍是同一套 iostream 缓冲,不存在混用问题。
9. 延伸阅读
- std::ios_base::sync_with_stdio —— 「必须在首次 I/O 之前调用」「关掉后混用是未定义/未指定行为」这两条都写在正文里。
- std::basic_ios::tie —— tie 的语义与默认绑定对象。
- std::format —— C++20 的格式化入口,配套的
std::vformat/std::print也在这里。 - 格式化语法(std::formatter) —— 填充、对齐、宽度、精度、进制前缀的完整形式化描述。
- C++ Core Guidelines · SL.io.50 Avoid endl —— 比
sync_with_stdio更值得先改的一条。
本知识库内的相关篇目:
- 《C++ iostream 格式化输出:setw 一次性、fixed 持久,两个坑一次分清》 —— iomanip 的操纵符真正难的不是记名字,而是分清谁只影响下一个输出项、谁会一直留在流的状态里。
- 《C++ stringstream 实用指南:分词解析、类型互转与 clear/str 的区别》 —— stringstream 是标准库里的「内存 IO」三件套
- 《C++ 文件读写实战:ofstream 默认清空文件、getline 与 >> 混用的坑》 —— ifstream / ofstream / fstream 的打开模式、状态位语义、RAII 自动关闭全
10. 一句话总结
sync_with_stdio(false)的收益主要在输入(3~6 倍),关掉之后printf和cout的缓冲互不知情、顺序不再有保证;要兼得类型安全、可读性和性能,就把格式化交给 C++20 的std::format。