探究算法竞赛中的输入输出效率

前言

在算法竞赛中,I/O有时是影响效率的瓶颈。I/O优化可以被模板化,与具体问题无关,是常数优化的重要方式之一。看到网上有很多关于这方面的文章,我也想来自己研究一下。

NOI系列目前支持的语言有C,C++和Pascal,其中C++可以直接使用C的绝大多数功能(但也有例外),因此下面只考虑C++和Pascal两种语言。通常,每种语言都提供了几种平台无关的I/O方式,如C++中的scanf/printfcin/coutfread/fwrite,Pascal中的read/writeblockread/blockwrite。也有一些低级的平台有关的I/O方式,Windows和Linux(Unix)都提供了内存映射文件,效率更加高。

在算法竞赛中,I/O通常用文本文件,而数据只有整数、浮点数和字符串三种。把十进制的字符串转换成整数或浮点数需要一定的时间,而分离出字符串也需要一定的时间,这就是I/O优化的方向。我们通常优化了时间,但是也降低了通用性

实验环境

为了保证通用性,我尽可能多的寻找平台测试。下面列举了所有用于实验的平台参数:

编号 操作系统 GCC版本 FPC版本
1(HOME) Windows 10 x64 7.1.0 3.0.2
2(HOME) ubuntu 14.04(NOI Linux) x86 4.8.4 2.6.2
3(SCHOOL) Windows 7 x64 7.1.0 2.6.2
4(HOME) Bash on Ubuntu on Windows(16.04) 5.4.0 3.0.0

整数的I/O

整数是算法竞赛中最常见的数据类型,大部分选手也只会写整数的I/O优化。下面列举进行的实验:

  • 输入$N$个整数,输出和,用于计算输入效率
  • 输入$N$个整数,输出这$N$个数,用空格间隔,可以计算输出效率
  • 输入$N$个整数,输出这$N$个数,用换行符间隔,计算另一种输出效率

注意task2和task3的区别,在很多题目中要求task3。

数据生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fstream>
#include <ctime>
#include <random>
using namespace std;
ofstream fout("integer.in");
const int n = 1e7;
int main()
{
minstd_rand gen(time(NULL));
fout << n << endl;
for (int i = 1; i <= n; i++)
{
uniform_int_distribution<> d(numeric_limits<int>::min(), numeric_limits<int>::max());
fout << d(gen) << ' ';
}
fout << endl;
return 0;
}

如果没有说明,数据规模均为$N=10^7$个整数。

系统I/O

C++的流

重定向的cin/cout

使用流来输入输出是C++的优点,有的选手采用这种用freopen重定向后,再使用cin/cout的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
freopen("integer.in", "r", stdin);
freopen("integer.out", "w", stdout);
int n;
cin >> n;
long long sum = 0;
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
sum += x;
}
cout << sum << endl;
return 0;
}
编号 task1 task2 task3
1 11,608ms 18,968ms 19,053ms
2 4,964ms 16,807ms 17,362ms
3

注意,task2和task3都已经减掉了task1的时间。可以发现,同等规模下输出比输入慢不少。

重定向的cin/cout+关同步

很多地方都有cin/cout关同步来加速。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
freopen("integer.in", "r", stdin);
freopen("integer.out", "w", stdout);
ios::sync_with_stdio(false);
int n;
cin >> n;
long long sum = 0;
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
sum += x;
}
cout << sum << endl;
return 0;
}
编号 task1 task2 task3
1 2,266ms 18,854ms 18,527ms
2 1,274ms 16,772ms 16,840ms
3

这次更加明显,输入比输出快8倍以上。关同步有效地加快了输入,但对于输出并没有效果。

文件流

文件流是C++中最适合处理文件输入输出的方式,但用的人并不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fstream>
using namespace std;
ifstream fin("integer.in");
ofstream fout("integer.out");
int main()
{
int n;
fin >> n;
long long sum = 0;
for (int i = 1; i <= n; i++)
{
int x;
fin >> x;
sum += x;
}
fout << sum << endl;
return 0;
}
编号 task1 task2 task3
1 2,172ms 1,826ms 18,639ms
2 1,187ms 1,129ms 16,374ms
3

文件流在task1上基本与上一种方法一样,但是task2快了10倍,task3和前面的一样慢。然而,把std::endl改成’\n’之后,task3就和task2一样快了。这是因为调用std::endl会执行flush操作。

C风格的格式化I/O

freopen重定向,并使用scanf/printf进行格式化输入输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdio>
int main()
{
freopen("integer.in", "r", stdin);
freopen("integer.out", "w", stdout);
int n;
scanf("%d", &n);
long long sum = 0;
for (int i = 1; i <= n; i++)
{
int x;
scanf("%d", &x);
sum += x;
}
printf("%lld\n", sum);
return 0;
}
编号 task1 task2 task3
1 8,234ms 3,187ms 3,261ms
2 1,171ms 1,137ms 1,163ms
3

Pascal的read/write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Program IOtest;

Var
n,i,x: longint;
sum: int64;
Begin
assign(input,'integer.in');
reset(input);
assign(output,'integer.out');
rewrite(output);
read(n);
sum := 0;
For i:=1 To n Do
Begin
read(x);
inc(sum,x);
End;
writeln(sum);
close(input);
close(output);
End.

编号 task1 task2 task3
1 2,361ms 2,490ms 2,499ms
2 1,495ms 1,502ms 1,592ms
3

Pascal的I/O比文件流稍慢一些,但还是很快的。

总结

系统I/O的速度与平台有很大的关系,所以我们要尽可能多找实验平台测试。在我家里的Windows下的结果如下:

I/O优化

I/O优化以牺牲通用性为代价来提高效率,在OI中很常用。其核心思想是:利用一个逐字符输入/输出函数,手动实现字符与整数的转化,也就是十进制与二进制的转化。系统I/O要考虑各种特殊情况,所以效率低下。下面的这段代码使用scanf输入字符串,再用atoi把字符串转为整数,但是比直接用scanf快一些,就体现了上述内容。这可以算是原始的输入优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cstdio>
#include <cstdlib>
int main()
{
freopen("integer.in", "r", stdin);
freopen("integer.out", "w", stdout);
int n;
scanf("%d", &n);
long long sum = 0;
for (int i = 1; i <= n; i++)
{
char buf[20];
scanf("%s", buf);
sum += atoi(buf);
}
printf("%lld\n", sum);
return 0;
}

普通I/O优化

基于最近一次模拟赛的代码,下面是大部分人用的输入优化模板,但是没有输出优化。于是我使用了szb’s version。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
inline int read()
{
int k = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
k = k * 10 + ch - '0';
ch = getchar();
}
return k * f;
}
inline void write(int x)
{
if (x < 0)
putchar('-'), x = -x;
if (x >= 10)
write(x / 10);
putchar(x % 10 + '0');
}
inline void writeln(int x)
{
write(x);
puts("");
}
编号 task1 task2 task3
1 2,983ms 3,188ms 3,438ms
2
3

基于fread/fwrite的I/O优化

本来想用别人的,但发现没有写输出优化,于是上我自己在模拟赛时写的最新版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <cstdio>
#include <cctype>
FILE *fin = fopen("integer.in", "r"), *fout = fopen("integer.out", "w");
const int SZ = 1e6;
char buf[SZ], *p = buf, *pend = buf;
inline int nextchar()
{
if (p == pend)
{
pend = (p = buf) + fread(buf, 1, SZ, fin);
if (pend == buf)
return EOF;
}
return *p++;
}
template <typename Int>
inline void read(Int &x)
{
char c = nextchar();
for (; isspace(c); c = nextchar())
;
x = 0;
Int sign = 1;
if (c == '-')
{
sign = -1;
c = nextchar();
}
for (; isdigit(c); c = nextchar())
x = x * 10 + c - '0';
x *= sign;
}
inline void writechar(char c)
{
if (p == pend)
{
fwrite(buf, 1, SZ, fout);
p = buf;
}
*p++ = c;
}
inline void flush()
{
fwrite(buf, 1, p - buf, fout);
}
int dig[20];
template <typename Int>
inline void writeln(Int x)
{
if (x < 0)
{
writechar('-');
x = -x;
}
int len = 0;
do
dig[++len] = x % 10;
while (x /= 10);
for (; len; len--)
writechar(dig[len] + '0');
writechar('\n');
}
int main()
{
int n;
read(n);
long long sum = 0;
for (int i = 1; i <= n; i++)
{
int x;
read(x);
sum += x;
}
writeln(sum);
flush();
return 0;
}
编号 task1 task2 task3
1 296ms 485ms 609ms
2
3

缓冲区的小优化

内存映射

Pascal