提高C++的cin/cout效率

cin.tie与sync_with_stdio加速输入输出

以前碰到cin TLE的时候总是傻乎乎地改成scanf,甚至还相信过C++在IO方面效率低下的鬼话,殊不知这只是C++为了兼容C而采取的保守措施。

tie

tie是将两个stream绑定的函数,空参数的话返回当前的输出流指针。

#include <iostream>
#include <fstream> 
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
    std::ostream *prevstr;
    std::ofstream ofs;
    ofs.open("test.txt");
    std::cout << "tie example:\n";  // 直接输出到屏幕
    *std::cin.tie() << "This is inserted into cout\n";  
    // 空参数调用返回默认的output stream,也就是cout  
    prevstr = std::cin.tie(&ofs);   
    // cin绑定ofs,返回原来的output stream  
    *std::cin.tie() << "This is inserted into the file\n";  
    // ofs,输出到文件    
    std::cin.tie(prevstr);                              
    // 恢复 
    ofs.close();
    system("pause");
    return 0;
}
///////////////////////////End Sub//////////////////////////////////

输出:

tie example:
This is inserted into cout请按任意键继续. . .

同时当前目录下的test.txt输出:

This is inserted into the file

sync_with_stdio

这个函数是一个“是否兼容stdio”的开关,C++为了兼容C,保证程序在使用了std::printf和std::cout的时候不发生混乱,将输出流绑到了一起。

应用

在ACM里,经常出现数据集超大造成 cin TLE的情况。这时候大部分人(包括原来我也是)认为这是cin的效率不及scanf的错,甚至还上升到C语言和C++语言的执行效率层面的无聊争论。其实像上文所说,这只是C++为了兼容而采取的保守措施。我们可以在IO之前将stdio解除绑定,这样做了之后要注意不要同时混用cout和printf之类。

在默认的情况下cin绑定的是cout,每次执行 << 操作符的时候都要调用flush,这样会增加IO负担。可以通过tie(0)(0表示NULL)来解除cin与cout的绑定,进一步加快执行效率。

如下所示:

#include <iostream>
int main() 
{
    std::ios::sync_with_stdio(false);   
    std::cin.tie(0);    // IO
}

reference:

http://meme.biology.tohoku.ac.jp/students/iwasaki/cxx/speed.html

C++的輸出入cin/cout和scanf/printf誰比較快?

有打過資訊競賽的人,一定有遇過用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’<<flush;,對,endl就是換行加上flush,也就是說,如果我們用endl的話,就會強迫每個數字都清空緩衝區,累積一定量再一起輸出對cout來說可以優化一些操作,而這樣就破壞了這個優化了,我們試著把endl拿掉試試看。

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到螢幕上的話,使用者就看不到了,而且你可能不想要換行,就算加<<flush也很麻煩,所以C++就設計了這樣的作法,讓你在cin前會把cout清空緩衝區。

無論如何,如果我們要對檔案輸入輸出很顯然不需要這樣,所以就解綁吧!

總結

我們試著把數字範圍放大到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快,我覺得也不會很奇怪。

参考

[1] http://www.hankcs.com/program/cpp/cin-tie-with-sync_with_stdio-acceleration-input-and-output.html

[2] http://chino.taipei/note-2016-0311C-%E7%9A%84%E8%BC%B8%E5%87%BA%E5%85%A5cin-cout%E5%92%8Cscanf-printf%E8%AA%B0%E6%AF%94%E8%BC%83%E5%BF%AB%EF%BC%9F/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • 自然衝擊療法由丁愚仁老師發明,又稱「禪拍」,「拍打」,"自然拍打"。 丁師及其團隊總結經驗,不同的各種病症(含絕症...
    YouAreMyMusic阅读 2,098评论 0 4
  • 今天要給大家分享的書是托馬斯戈登博士寫的「PET父母效能訓練手冊」,這本書的作者托馬斯戈登博士曾於1997、199...
    zoewyc阅读 286评论 0 0
  • 一一與旋轉屋 徐空文 (這是十幾年前創作的第一個劇本,雖然幼稚,但現在看來竟是我最喜歡的劇本之一,雖然之後曾以寫劇...
    徐空文阅读 480评论 0 5
  • 胡益达阅读 406评论 0 0