有打竞赛或经常刷题的人,一定有遇过用cin/cout结果TLE,换成scanf/printf就AC的情况。
难道cin/cout真的比较慢吗?为什么C++要做出一个比C还要更慢的输入输出介面呢?
我们来看看cin/cout的效率到底怎么样。
以下都是个人的观察,有错的话请留言告知QAQ,本人很废还请鞭小力一点。
开始前,cin/cout是什么?
首先,我们先来看一下cin/cout和scanf/printf的差别,前者是物件,后者是函数。
函数很简单,就是定义一个函数,然后他会把里面出现%的地方取代掉,而物件则是重载了shift运算子<<,>>,其实真的很直观,就丢进cout跟从cin拿出来嘛~,而且也不用管型别,因为编译器会帮你找运算子规则。
这里我们发现,型别是编译器处理的,和执行时完全没有关系(别再说cin/cout慢是因为要判断型别了),而且自由度更高,可以自己定义。
那cin/cout到底慢再哪里呢?
我们先用time指令做个小实验,在Ubuntu 14.04笔电对一个档案写入1e7的random整数,这里的程式码都是简化的code。
for(int i = 0; i < (int)1e7; i++){
printf("%d\n",rand());
}
// vs
for(int i = 0; i < (int)1e7; i++){
cout<<rand()<<endl;
}
实验三次,printf的时间分别是,
1.760 s
2.677 s
1.865 s
看起来很优秀,那cout呢?
15.921 s
15.188 s
15.685 s
发生了什么事?怎么慢成这样!
优化1:sync_with_stdio 函数:和stdio同步
我已经看到那些笃定cin/cout不好的人偷笑的表情了,但是事情别说的太早,我们先看一下C++ Reference对于cin/cout的说明,我们发现了一个函数:std::ios_base ::sync_with_stdio(false),他是这样说的
Toggles on or off synchronization of all the iostream standard streams with their corresponding standard C streams if it is called before the program performs its first input or output operation.
If called once an input or output operation has occurred, its effects are implementation-defined.
By default, iostream objects and cstdio streams are synchronized (as if this function was called with true as argument).
With stdio synchronization turned off, iostream standard stream objects may operate independently of the standard C streams (although they are not required to), and mixing operations may result in unexpectedly interleaved characters.
看起来,cin/cout预设必须要跟stdin/stdout同步,所以必须做额外的运算,注意要是关掉了,scanf/printf就不能用了(如果用了,而且跟cin/cout混用,可能会吃到奇怪的东西),那我们试着把他关掉看看。
ios_base::sync_with_stdio(false);
for(int i = 0; i < (int)1e7; i++){
cout<<rand()<<endl;
}
结果:
13.120 s
14.958 s
15.165 s
看起来变快了两秒,甚至根本没变快,还是很慢啊…(你看看,自己慢还怪scanf/printf拖慢你)
等等,我们还忘了一个东西,endl。
优化2:endl 和 flush 物件:cout的缓冲区优化
什么是endl,他是一个定义好的物件,在cout上给cout换行用的,那他跟<<’\n’有什么差别呢?
原来,cout用了一个类似优化的设计,叫作缓冲区(由作业系统实作),所有的输出都会先进到缓冲区里,直到缓冲区满了才会清空缓冲区并把字串输出到stdout之类的输出串流,难怪没有跟stdout同步会出错。
而当一般人写程式的时候,输出当然希望程式会把东西印到萤幕上,但是如果缓冲区还没满,我们就看不到结果了!
怎么办呢? cout有一个物件叫作flush(用法跟endl一样),做的事情就是强迫清空缓冲区,并输出到串流。
但是为什么平常出学C++的人都没有打过flush呢?原因有几个,一个是Windows8以前的Windows CMD会自动清空缓冲区(或是根本没有QAQ),另外一个主要的原因就是,其实endl就是<<’\n’<
ios_base::sync_with_stdio(false);
for(int i = 0; i < (int)1e7; i++){
cout<<rand()<<'\n';
}
结果:
2.765 s
1.708 s
1.713 s
太震惊了,去掉了endl之后,cout的速度已经和printf差不多快了! !整整快了12秒! !
原来效率就是在这种情况下不见的,那为什么要作endl这种物件呢?
我们看看下面的实验。
附注,其实printf也是有缓冲区的,只是他预设是到满了才会清空。平常在console可以看到输出是因为OS帮忙我们把缓冲区清掉了
优化3:cin.tie(0):cin和cout绑定
我们先吃一个数字进来,再把他输出
for(int i = 0; i < (int)1e7; i++){
scanf("%d\n",&a);
printf("%d\n",a+1); //output a+1;
}
// vs
for(int i = 0; i < (int)1e7; i++){
cin>>a;
cout<<a+1<<endl;
}
scanf/printf的时间:
2.579 s
3.994 s
3.241 s
而cin/cout:
19.970 s
不意外,那加上关闭同步的话?
ios_base::sync_with_stdio(false);
for(int i = 0; i < (int)1e7; i++){
cin>>a;
cout<<a+1<<endl;
}
结果:
16.575 s
快了几秒,不算太意外,那去掉endl呢?
ios_base::sync_with_stdio(false);
for(int i = 0; i < (int)1e7; i++){
cin>>a;
cout<<a+1<<'\n';
}
结果:
16.408 s
什么! !完全没有变快啊! ? (你看看,看来就算cout很快,cin还是很慢啊)
等等,已经说过cin没道理比scanf慢这么多,所以我们来看看发生了什么事。
既然和有endl一样快,我们可以合理怀疑是cin/cout又清空缓冲区了。
我们试试看下面的例子,我们先吃进一个阵列,再丢出来。
ios_base::sync_with_stdio(false);
for(int i = 0; i < (int)1e7; i++){
cin>>A[i];
}
for(int i = 0; i < (int)1e7; i++){
cout<<A[i]+1<<'\n';
}
结果:
2.918 s
2.811 s
3.062 s
太神奇了,竟然变得什至比scanf/printf还要快了,发生了什么事?
看起来是cin/cout交错使用导致的,我们看一下C++ Reference对于cin的说明,我们发现一个函数tie()。
std::ios::tie
Get/set tied stream
The tied stream is an output stream object which is flushed before each i/o operation in this stream object.
这样就清楚了,cin预设绑住了cout,而被绑住的ostream会在istream要输入时被flush。
那我们试试看把cin/cout解绑,我们可以透过传一个NULL(也可以用0)进入cin.tie()来让cin绑住空的ostream。
我们加上一行cin.tie(0)再来看刚刚的例子。
ios_base::sync_with_stdio(false);
cin.tie(0);
for(int i = 0; i < (int)1e7; i++){
cin>>a;
cout<<a+1<<'\n';
}
结果:
2.956 s
2.889 s
3.509 s
时间已经和吃进阵列差不多了,剩下的差距已经在误差范围内了。
为什么要有tie这个设计呢?
我曾经看过一些说法,一种是说,因为我们有时候可能要写一些console应用程式,如果我们要使用者输入一些值的时候可能要先输出一些提示讯息像是「请输入一个数字:」然后才用cin输入,要是上面那一句话没有被flush到萤幕上的话,使用者就看不到了,而且你可能不想要换行,就算加<
总结
我们试着把数字范围放大到1e8看看,
scanf/printf:
30.722 s
29.428 s
cin/cout:
27.052 s
27.097 s
cin/cout的表现已经比scanf/printf好了,事实上,我之前看过一篇文章(现在找不到了QQ)里面有一张图表,上面显示了cin/cout的效率在1e7之后就会开始超越scanf /printf了,当然这有很多的因素在里面,而且iostream使用的记忆体也比scanf/printf高出一些。
但总结来说cin/cout和scanf/printf比起来更快最主要的原因,是cin/cout可以在编译时期就把型别等等编译进去,而scanf/printf则要在执行时期处理,所以cin /cout就算比scanf/printf快,我觉得也不会很奇怪。