關於讀入優化的分析

關於讀入優化的分析

摘要

身爲一隻以卡常爲生的蒟蒻,總想着經過一些奇技淫巧來掩飾優化常數。ios

因而本文章就非正式地從最初的開始到最終的終止來談談在OI種各類主流、非主流讀入的速度以及利弊。c++

序言

隨着算法的發展各類數據結構等勁題出現,這些題除了思惟難度的提升還帶來者輸入數據的增多(特別的有:uoj.ac上的一道題須要選手本身生成數據,而數論題每每輸入較少),對於有追求有理想的選手快速讀入是必不可少的技能。算法

儘管市面上有不一樣的主流讀入優化,可是大多都是基於fread的,其他的只是一些小變更。ubuntu

而筆者就在不久以前發現更快可是非主流的mmap(在sys/mman.h中)函數,此函數比目前已知全部讀入都快。數組

如今,咱們從入門的cin_with_sync(true)而後到進階的cin_with_sync(false),再到標準的scanf而後到getchar,再到fread(old),再是fread(new),最後是mmap的原理及分析。緩存

標準

本次測試在如下環境進行:數據結構

  1. 硬件:函數

    a) VM WorkingStation Pro 14虛擬機測試

    b) 基於Ubuntu 14.04 LTS 32位 的NOI Linux 1.4.1優化

    c) 內存1003.1MiB,硬盤19.9GB,CPU Intel® Core™ i7-6498DU CPU @ 2.50GHz,GPU Gallium 0.4 on SVGA3D; build: RELEASE;

  2. 軟件: a) 編譯器G++ posix gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04)

    b) 測評器:Project Lemon v1.2 測試版

    c) 編譯命令:g++ -o %s %s.*(不加入-std=c++11的緣由是由於c++11標準會忽略部分例如register的語句,同時NOI的編譯命令也沒有此條)

  3. 文件: a) 輸入文件:兩組,大小分別爲127585438 Byte 和 127582201 Byte,前半部分爲11111111個不超過INT_MAX(在climits內)的非負整數,用空格分隔,中間一個換行符,緊接着一行由11111111個id>=48的字符組成。

    b) 輸出文件:爲了不代碼的部分被過度優化,最後程序將根據輸入計算一個值,而後輸出這個值。詳見代碼。

代碼

如下代碼儘可能按照最快的方式儘可能寫成函數:

 

  1 //cin_with_sync(true)
  2 #include <cstdio>
  3 #include <iostream>
  4 
  5 using namespace std;
  6 
  7 #define MAXN 11111111
  8 
  9 inline int test(){
 10     int recieve_int, ret = 0;
 11     for(int i = 0; i < MAXN; i++){
 12         cin >> recieve_int;
 13         ret += recieve_int;
 14     }
 15     char recieve_char;
 16     for(int i = 0; i < MAXN; i++){
 17         cin >> recieve_char;
 18         ret -= recieve_char;
 19     }
 20     return ret + 1;
 21 }
 22 
 23 
 24 int main(){
 25     freopen("fr.in", "r", stdin);
 26     printf("%d", test());
 27     fclose(stdin);
 28     return 0;
 29 }
 30 //cin_with_sync(false)
 31 #include <cstdio>
 32 #include <iostream>
 33 
 34 using namespace std;
 35 
 36 #define MAXN 11111111
 37 
 38 inline int test(){
 39     ios::sync_with_stdio(false);
 40     cin.tie(0);
 41     int recieve_int, ret = 0;
 42     for(int i = 0; i < MAXN; i++){
 43         cin >> recieve_int;
 44         ret += recieve_int;
 45     }
 46     char recieve_char;
 47     for(int i = 0; i < MAXN; i++){
 48         cin >> recieve_char;
 49         ret -= recieve_char;
 50     }
 51     return ret + 1;
 52 }
 53 
 54 
 55 int main(){
 56     freopen("fr.in", "r", stdin);
 57     printf("%d", test());
 58     fclose(stdin);
 59     return 0;
 60 }
 61 //scanf
 62 #include <cstdio>
 63 
 64 using namespace std;
 65 
 66 #define MAXN 11111111
 67 
 68 inline int test(){
 69     int recieve_int, ret = 0;
 70     for(int i = 0; i < MAXN; i++){
 71         scanf("%d", &recieve_int);
 72         ret += recieve_int;
 73     }
 74     char recieve_char;
 75     scanf("%c", &recieve_char), scanf("%c", &recieve_char);
 76     for(int i = 0; i < MAXN; i++){
 77         scanf("%c", &recieve_char);
 78         ret -= recieve_char;
 79     }
 80     return ret + 1;
 81 }
 82 
 83 
 84 int main(){
 85     freopen("fr.in", "r", stdin);
 86     printf("%d", test());
 87     fclose(stdin);
 88     return 0;
 89 }
 90 //getchar
 91 #include <cstdio>
 92 
 93 using namespace std;
 94 
 95 #define MAXN 11111111
 96 
 97 inline int read(){
 98     int num = 0;
 99     char c;
100     while((c = getchar()) < 48);
101     while(num = num * 10 + c - 48, (c = getchar()) >= 48);
102     return num;
103 }
104 
105 inline int test(){
106     int recieve_int, ret = 0;
107     for(int i = 0; i < MAXN; i++){
108         recieve_int = read();
109         ret += recieve_int;
110     }
111     char recieve_char;
112     while((recieve_char = getchar()) < 60);
113     ret -= recieve_char;
114     for(int i = 0; i < MAXN; i++){
115         recieve_char = getchar();
116         ret -= recieve_char;
117     }
118     return ret;
119 }
120 
121 
122 int main(){
123     freopen("fr.in", "r", stdin);
124     printf("%d", test());
125     fclose(stdin);
126     return 0;
127 }
128 //fread(old)
129 #include <cstdio>
130 
131 using namespace std;
132 
133 #define MAXN 11111111
134 
135 #define Finline __inline__ __attribute__ ((always_inline))
136 
137 Finline char get_char(){
138     static char buf[200000001], *p1 = buf, *p2 = buf;
139     return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 200000000, stdin), p1 == p2) ? EOF : *p1 ++;
140 }
141 inline int read(){
142     int num = 0;
143     char c;
144     while((c = get_char()) < 48);
145     while(num = num * 10 + c - 48, (c = get_char()) >= 48);
146     return num;
147 }
148 
149 inline int test(){
150     int recieve_int, ret = 0;
151     for(int i = 0; i < MAXN; i++){
152         recieve_int = read();
153         ret += recieve_int;
154     }
155     char recieve_char;
156     while((recieve_char = get_char()) < 60);
157     ret -= recieve_char;
158     for(int i = 0; i < MAXN; i++){
159         recieve_char = get_char();
160         ret -= recieve_char;
161     }
162     return ret;
163 }
164 
165 
166 int main(){
167     freopen("fr.in", "r", stdin);
168     printf("%d", test());
169     fclose(stdin);
170     return 0;
171 }
172 //fread(new)
173 #include <cstdio>
174 
175 using namespace std;
176 
177 #define MAXN 11111111
178 
179 #define Finline __inline__ __attribute__ ((always_inline))
180 
181 Finline char get_char(){
182     static char buf[200000001], *p1 = buf, *p2 = buf + fread(buf, 1, 200000000, stdin);
183     return p1 == p2 ? EOF : *p1 ++;
184 }
185 inline int read(){
186     int num = 0;
187     char c;
188     while((c = get_char()) < 48);
189     while(num = num * 10 + c - 48, (c = get_char()) >= 48);
190     return num;
191 }
192 
193 inline int test(){
194     int recieve_int, ret = 0;
195     for(int i = 0; i < MAXN; i++){
196         recieve_int = read();
197         ret += recieve_int;
198     }
199     char recieve_char;
200     while((recieve_char = get_char()) < 60);
201     ret -= recieve_char;
202     for(int i = 0; i < MAXN; i++){
203         recieve_char = get_char();
204         ret -= recieve_char;
205     }
206     return ret;
207 }
208 
209 
210 int main(){
211     freopen("fr.in", "r", stdin);
212     printf("%d", test());
213     fclose(stdin);
214     return 0;
215 }
216 //mmap
217 #include <cstdio>
218 #include <fcntl.h>
219 #include <unistd.h>
220 #include <sys/mman.h>
221 
222 using namespace std;
223 
224 #define MAXN 11111111
225 
226 #define Finline __inline__ __attribute__ ((always_inline))
227 
228 char *pc;
229 
230 inline int read(){
231     int num = 0;
232     char c;
233     while ((c = *pc++) < 48);
234     while (num = num * 10 + c - 48, (c = *pc++) >= 48);
235     return num;
236 }
237 
238 inline int test(){
239     pc = (char *) mmap(NULL, lseek(0, 0, SEEK_END), PROT_READ, MAP_PRIVATE, 0, 0);
240     int recieve_int, ret = 0;
241     for(int i = 0; i < MAXN; i++){
242         recieve_int = read();
243         ret += recieve_int;
244     }
245     char recieve_char;
246     while((recieve_char = *pc++) < 60);
247     ret -= recieve_char;
248     for(int i = 0; i < MAXN; i++){
249         recieve_char = *pc++;
250         ret -= recieve_char;
251     }
252     return ret + 1;
253 }
254 
255 
256 int main(){
257     freopen("fr.in", "r", stdin);
258     printf("%d", test());
259     fclose(stdin);
260     return 0;
261 }
262 //數據生成器
263 #include <ctime>
264 #include <cstdio>
265 #include <climits>
266 #include <algorithm>
267 
268 #define MAXN 11111111
269 
270 int main(){
271     freopen("fr.in", "w", stdout);
272     srand(time(NULL));
273     for(int i = 0; i < MAXN; i++) printf("%d ", rand() % INT_MAX);
274     puts("");
275     for(int i = 0; i < MAXN; i++) putchar(rand() % 48 + 79);
276     fclose(stdout);
277     return 0;
278 }

 

結果

咱們在測評機下作了屢次試驗,調整了代碼的部分細節,取最後一次測試成績以下:

 

 

 

並且據最新試驗代表,在Luogu_OJ上對於正常輸入的題平均每1.5MB的輸入mmap和fread(new)具備時間差4ms

分析

cin_with_sync(true)當然慢,其緣由是由於爲了其保持和scanf等函數的輸出保持同步,因此一直會刷新流,因此當然慢。然而因爲cin「比較智能」,因此用它也有理由,並且使用cout輸出long long會比printf快很多。

因此cin_with_sync(false)會快很多的緣由同上。

然而咱們驚奇地發現scanf「居然」比cin_with_sync(false)慢!?其實在實際測試過程當中,二者的速度不相上下,都是差很少的。

getchar是筆者第一個學的快讀,而後其實在實際使用中這種快讀比scanf的優點更爲明顯,特別是在分散讀入的時候。然而如今二者跑出了僅差0.2s的成績,其實也不用驚訝,由於在之前的scanf的實現主要在loc_incl.h內的_doscan函數內,觀察這個函數發現它就是你的快讀的整合版。

fread(old)利用fread函數把全部輸入一次性輸入到程序內的緩存數組而後用getchar式快讀調用。好處就是文件操做少,於是速度快。

fread(new)和fread(old)的惟一區別就是fread(new)只讀了一次文件,而fread(old)容許讀屢次。fread(old)其實是爲了防止數據分幾回輸入,然而於是函數較長,不太有可能被inline優化。而fread(new)則能夠避免這些。同時在實際使用中,fread(new)也具備更快的速度。

mmap基於Linux的黑科技,直接將文件映射到內存操做,中間不須要阻塞系統調用,不須要內核緩存,只須要一次lseek,於是有更優的速度,是極限卡常者不二選擇。在fread(new)已經很是快的狀況下再甩36ms,並且實際使用的時候速度更快。

總結

對於初學者來講,cin和scanf足矣。

然而若是是在須要cin來讀字符串或者某些奇怪的東西的話,建議不要流同步。固然若是你知道流的概念以後也能夠靈活使用。

能夠發現getchar是全部方法中空間最小的,由於它的實現不須要scanf那樣把全部狀況枚舉出來,也不須要額外數組,適用於平常生活。

而fread是在各個平臺(實測在某些平臺上(例如Win32的部分機子)是會出現沒法讀入的狀況,但通常測評系統都會支持,因此在提交時能夠改掉)下均可以使用的一個比較快速的讀入方式,同時在gdb中,fread須要使用EOF,而這就能夠方便文本終端中一次性把數據輸入gdb。同時你能夠用緩存數組進行一些更高級的操做。

mmap只能在Linux下使用,並且不接受鍵盤讀入 ,建議在確保程序無誤後使用。

 

更新

鑑於在實際使用過程當中,有extern inline優化(更強的內聯優化),可是這種優化須要慎用。

因而咱們順便來說一下inline的做用。

inline是一種浪費空間而換取時間的玩意。

通常在沒有複雜語句,沒有循環,屢次調用的地方用。

對於通常的inline,編譯器經常會忽略這種優化,除非像這麼簡單(通常來講,帶有循環、遞歸、switch、goto、static都不會inline)的:

1 __asm__ __volatile__ ("\tmovq %1, %%rax \n\t imulq %2 \n\t idivq %3\n" : "=d"(ret): "m"(a), "m"(b), "m"(MODS) : "%rax");

而後對於static inline,編譯器會有所重視(固然詳細的信息須要深刻研究),對於__函數聲明前的調用__、遞歸調用、被經過地址應用的,編譯器會像普通函數同樣編譯。

對於extern inline,編譯器大部分都會展開,達到一個相似於宏函數的效果,可是比宏函數略微高級的是它採用的不是直接替換,例如#define pow(x) x * x,而後調用pow(10 + 10)就會出問題,可是extern inline不會。可是彷佛extern inline有時候使用會有問題,例如我就遇到過在extern inline過的函數對全局變量操做出錯的問題。

而後目前來講彷佛對於幾種快速讀入,筆者用起來仍是沒有問題的。固然這還受到代碼細節的影響。

 

協議

本做品採用知識共享署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議進行許可。

相關文章
相關標籤/搜索