1. 程式人生 > >關於讀入優化的最終分析

關於讀入優化的最終分析

緩存 文件映射 ice 開始 int 字符 1.4.1 clas man

關於讀入優化的最終分析

摘要

身為一只以卡常為生的蒟蒻,總想著通過一些奇技淫巧來掩飾優化常數。

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

序言

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

盡管市面上有不同的主流讀入優化,但是大多都是基於fread的,其余的只是一些小變動。

而筆者就在不久之前發現更快但是非主流的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 }

結果

我們在測評機下做了多次試驗,調整了代碼的部分細節,取最後一次測試成績如下:

技術分享圖片

分析

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)已經非常快的情況下再甩36m,而且實際使用的時候速度更快。

總結

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

可以發現getchar是所有方法中空間最小的,因為它的實現不需要scanf那樣把所有情況枚舉出來,也不需要額外數組,適用於日常生活。

而fread是在各個平臺下都可以使用的一個比較快速的讀入方式,同時在gdb中,fread需要使用EOF,而這就可以方便文本終端中一次性把數據輸入gdb。同時你可以用緩存數組進行一些更高級的操作。

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

協議

技術分享圖片

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

更為舒服地看這篇文章:https://www.luogu.org/blog/CreeperLKF/FastRead

關於讀入優化的最終分析