探索C++最快的读取文件的方案

http://www.byvoid.com/blog/fast-readfile/

 

http://blog.csdn.net/jifengszf/article/details/3886802

 

在竞赛中,遇到大数据时,往往读文件成了程序运行速度的瓶颈,需要更快的读取方式。相信几乎所有的C++学习者都在cin机器缓慢的速度上栽过跟头,于是从此以后发誓不用cin读数据。还有人说Pascal的read语句的速度是C/C++中scanf比不上的,C++选手只能干着急。难道C++真的低Pascal一等吗?答案是不言而喻的。一个进阶的方法是把数据一下子读进来,然后再转化字符串,这种方法传说中很不错,但具体如何从没试过,因此今天就索性把能想到的所有的读数据的方式都测试了一边,结果是惊人的。

竞赛中读数据的情况最多的莫过于读一大堆整数了,于是我写了一个程序,生成一千万个随机数到data.txt中,一共55MB。然后我写了个程序主干计算运行时间,代码如下:

 
 
1
2
3
4
5
6
7
#include <ctime>
int main()
{
	int start = clock();
	//DO SOMETHING
	printf("%.3lf\n",double(clock()-start)/CLOCKS_PER_SEC);
}

最简单的方法就算写一个循环scanf了,代码如下:

1
2
3
4
5
6
7
8
9
10
const int MAXN = 10000000;
 
int numbers[MAXN];
 
void scanf_read()
{
	freopen("data.txt","r",stdin);
	for (int i=0;i<MAXN;i++)
		scanf("%d",&numbers[i]);
}

可是效率如何呢?在我的电脑Linux平台上测试结果为2.01秒。接下来是cin,代码如下

1
2
3
4
5
6
7
8
9
10
const int MAXN = 10000000;
 
int numbers[MAXN];
 
void cin_read()
{
	freopen("data.txt","r",stdin);
	for (int i=0;i<MAXN;i++)
		std::cin >> numbers[i];
}

出乎我的意料,cin仅仅用了6.38秒,比我想象的要快。cin慢是有原因的,其实默认的时候,cin与stdin总是保持同步的,也就是说这两种方法可以混用,而不必担心文件指针混乱,同时cout和stdout也一样,两者混用不会输出顺序错乱。正因为这个兼容性的特性,导致cin有许多额外的开销,如何禁用这个特性呢?只需一个语句std::ios::sync_with_stdio(false);,这样就可以取消cin于stdin的同步了。程序如下:

1
2
3
4
5
6
7
8
9
10
11
const int MAXN = 10000000;
 
int numbers[MAXN];
 
void cin_read_nosync()
{
	freopen("data.txt","r",stdin);
	std::ios::sync_with_stdio(false);
	for (int i=0;i<MAXN;i++)
		std::cin >> numbers[i];
}

取消同步后效率究竟如何?经测试运行时间锐减到了2.05秒,与scanf效率相差无几了!有了这个以后可以放心使用cin和cout了。

接下来让我们测试一下读入整个文件再处理的方法,首先要写一个字符串转化为数组的函数,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const int MAXS = 60*1024*1024;
char buf[MAXS];
 
void analyse(char *buf,int len = MAXS)
{
	int i;
	numbers[i=0]=0;
	for (char *p=buf;*p && p-buf<len;p++)
		if (*p == ' ')
			numbers[++i]=0;
		else
			numbers[i] = numbers[i] * 10 + *p - '0';
}

把整个文件读入一个字符串最常用的方法是用fread,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const int MAXN = 10000000;
const int MAXS = 60*1024*1024;
 
int numbers[MAXN];
char buf[MAXS];
 
void fread_analyse()
{
	freopen("data.txt","rb",stdin);
	int len = fread(buf,1,MAXS,stdin);
	buf[len] = '\0';
	analyse(buf,len);
}

上述代码有着惊人的效率,经测试读取这10000000个数只用了0.29秒,效率提高了几乎10倍!掌握着种方法简直无敌了,不过,我记得fread是封装过的read,如果直接使用read,是不是更快呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const int MAXN = 10000000;
const int MAXS = 60*1024*1024;
 
int numbers[MAXN];
char buf[MAXS];
 
void read_analyse()
{
	int fd = open("data.txt",O_RDONLY);
	int len = read(fd,buf,MAXS);
	buf[len] = '\0';
	analyse(buf,len);
}

测试发现运行时间仍然是0.29秒,可见read不具备特殊的优势。到此已经结束了吗?不,我可以调用Linux的底层函数mmap,这个函数的功能是将文件映射到内存,是所有读文件方法都要封装的基础方法,直接使用mmap会怎样呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
const int MAXN = 10000000;
const int MAXS = 60*1024*1024;
 
int numbers[MAXN];
char buf[MAXS];
void mmap_analyse()
{
	int fd = open("data.txt",O_RDONLY);
	int len = lseek(fd,0,SEEK_END);
	char *mbuf = (char *) mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,0);	
	analyse(mbuf,len);
}

经测试,运行时间缩短到了0.25秒,效率继续提高了14%。到此为止我已经没有更好的方法继续提高读文件的速度了。回头测一下Pascal的速度如何?结果令人大跌眼镜,居然运行了2.16秒之多。程序如下:

?View Code PASCAL
1
2
3
4
5
6
7
8
9
10
11
const
	MAXN = 10000000;
var
	numbers :array[0..MAXN] of longint;
	i :longint;
begin
	assign(input,'data.txt');
	reset(input);
	for i:=0 to MAXN do
		read(numbers[i]);
end.

为确保准确性,我又换到Windows平台上测试了一下。结果如下表:

方法/平台/时间(秒) Linux gcc Windows mingw Windows VC2008
scanf 2.010 3.704 3.425
cin 6.380 64.003 19.208
cin取消同步 2.050 6.004 19.616
fread 0.290 0.241 0.304
read 0.290 0.398 不支持
mmap 0.250 不支持 不支持
Pascal read 2.160 4.668  

从上面可以看出几个问题

  1. Linux平台上运行程序普遍比Windows上快。
  2. Windows下VC编译的程序一般运行比MINGW(MINimal Gcc for Windows)快。
  3. VC对cin取消同步与否不敏感,前后效率相同。反过来MINGW则非常敏感,前后效率相差8倍。
  4. read本是linux系统函数,MINGW可能采用了某种模拟方式,read比fread更慢。
  5. Pascal程序运行速度实在令人不敢恭维

 

////////////////////////////////////////////////////////////////////////////////

 

 

在竞赛中,遇到大数据时,往往读文件成了程序运行速度的瓶颈,需要更快的读取方式。相信几乎所有的C++学习者都在cin机器缓慢的速度上栽过跟头,于是从此以后发誓不用cin读数据。还有人说Pascal的read语句的速度是C/C++中scanf比不上的,C++选手只能干着急。难道C++真的低Pascal一等吗?答案是不言而喻的。一个进阶的方法是把数据一下子读进来,然后再转化字符串,这种方法传说中很不错,但具体如何从没试过,因此今天就索性把能想到的所有的读数据的方式都测试了一边,结果是惊人的。

竞赛中读数据的情况最多的莫过于读一大堆整数了,于是我写了一个程序,生成一千万个随机数到data.txt中,一共55MB。然后我写了个程序主干计算运行时间,代码如下:

1
2
3
4
5
6
7
#include <ctime>
int main()
{
	int start = clock();
	//DO SOMETHING
	printf("%.3lf\n",double(clock()-start)/CLOCKS_PER_SEC);
}

最简单的方法就算写一个循环scanf了,代码如下:

1
2
3
4
5
6
7
8
9
10
const int MAXN = 10000000;
 
int numbers[MAXN];
 
void scanf_read()
{
	freopen("data.txt","r",stdin);
	for (int i=0;i<MAXN;i++)
		scanf("%d",&numbers[i]);
}

可是效率如何呢?在我的电脑Linux平台上测试结果为2.01秒。接下来是cin,代码如下

1
2
3
4
5
6
7
8
9
10
const int MAXN = 10000000;
 
int numbers[MAXN];
 
void cin_read()
{
	freopen("data.txt","r",stdin);
	for (int i=0;i<MAXN;i++)
		std::cin >> numbers[i];
}

出乎我的意料,cin仅仅用了6.38秒,比我想象的要快。cin慢是有原因的,其实默认的时候,cin与stdin总是保持同步的,也就是说这两种方法可以混用,而不必担心文件指针混乱,同时cout和stdout也一样,两者混用不会输出顺序错乱。正因为这个兼容性的特性,导致cin有许多额外的开销,如何禁用这个特性呢?只需一个语句std::ios::sync_with_stdio(false);,这样就可以取消cin于stdin的同步了。程序如下:

1
2
3
4
5
6
7
8
9
10
11
const int MAXN = 10000000;
 
int numbers[MAXN];
 
void cin_read_nosync()
{
	freopen("data.txt","r",stdin);
	std::ios::sync_with_stdio(false);
	for (int i=0;i<MAXN;i++)
		std::cin >> numbers[i];
}

取消同步后效率究竟如何?经测试运行时间锐减到了2.05秒,与scanf效率相差无几了!有了这个以后可以放心使用cin和cout了。

接下来让我们测试一下读入整个文件再处理的方法,首先要写一个字符串转化为数组的函数,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const int MAXS = 60*1024*1024;
char buf[MAXS];
 
void analyse(char *buf,int len = MAXS)
{
	int i;
	numbers[i=0]=0;
	for (char *p=buf;*p && p-buf<len;p++)
		if (*p == ' ')
			numbers[++i]=0;
		else
			numbers[i] = numbers[i] * 10 + *p - '0';
}

把整个文件读入一个字符串最常用的方法是用fread,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const int MAXN = 10000000;
const int MAXS = 60*1024*1024;
 
int numbers[MAXN];
char buf[MAXS];
 
void fread_analyse()
{
	freopen("data.txt","rb",stdin);
	int len = fread(buf,1,MAXS,stdin);
	buf[len] = '\0';
	analyse(buf,len);
}

上述代码有着惊人的效率,经测试读取这10000000个数只用了0.29秒,效率提高了几乎10倍!掌握着种方法简直无敌了,不过,我记得fread是封装过的read,如果直接使用read,是不是更快呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const int MAXN = 10000000;
const int MAXS = 60*1024*1024;
 
int numbers[MAXN];
char buf[MAXS];
 
void read_analyse()
{
	int fd = open("data.txt",O_RDONLY);
	int len = read(fd,buf,MAXS);
	buf[len] = '\0';
	analyse(buf,len);
}

测试发现运行时间仍然是0.29秒,可见read不具备特殊的优势。到此已经结束了吗?不,我可以调用Linux的底层函数mmap,这个函数的功能是将文件映射到内存,是所有读文件方法都要封装的基础方法,直接使用mmap会怎样呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
const int MAXN = 10000000;
const int MAXS = 60*1024*1024;
 
int numbers[MAXN];
char buf[MAXS];
void mmap_analyse()
{
	int fd = open("data.txt",O_RDONLY);
	int len = lseek(fd,0,SEEK_END);
	char *mbuf = (char *) mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,0);	
	analyse(mbuf,len);
}

经测试,运行时间缩短到了0.25秒,效率继续提高了14%。到此为止我已经没有更好的方法继续提高读文件的速度了。回头测一下Pascal的速度如何?结果令人大跌眼镜,居然运行了2.16秒之多。程序如下:

?View Code PASCAL
1
2
3
4
5
6
7
8
9
10
11
const
	MAXN = 10000000;
var
	numbers :array[0..MAXN] of longint;
	i :longint;
begin
	assign(input,'data.txt');
	reset(input);
	for i:=0 to MAXN do
		read(numbers[i]);
end.

为确保准确性,我又换到Windows平台上测试了一下。结果如下表:

 

方法/平台/时间(秒) Linux gcc Windows mingw Windows VC2008
scanf 2.010 3.704 3.425
cin 6.380 64.003 19.208
cin取消同步 2.050 6.004 19.616
fread 0.290 0.241 0.304
read 0.290 0.398 不支持
mmap 0.250 不支持 不支持
Pascal read 2.160 4.668  

从上面可以看出几个问题

  1. Linux平台上运行程序普遍比Windows上快。
  2. Windows下VC编译的程序一般运行比MINGW(MINimal Gcc for Windows)快。
  3. VC对cin取消同步与否不敏感,前后效率相同。反过来MINGW则非常敏感,前后效率相差8倍。
  4. read本是linux系统函数,MINGW可能采用了某种模拟方式,read比fread更慢。
  5. Pascal程序运行速度实在令人不敢恭维。

希望此文能对大家有所启发,欢迎与我继续讨论。

////////////////////////////////////////////////////////////////////////////

一. 文件一次读入速度

linux下读文件这东西最后都是要通过系统调用sys_read(fd,buf,count)来实现的,所以如果要提高速度,就是最简单地调用sys_read的封装,比如直接用read()或fread()。下面是我在linux下的几个测试。

首先创建一个130M数据文件 dd if=/dev/zero of=data bs=1024k count=130
分别用fread,read和fgets一次读入全部大小文件所消耗时间对比,其中
size=130*1024*1024
char *buf=new char[size];

下面是测试结果(机器Intel(R) Pentium(R) 4 CPU 3.20GHz, Mem 1G):
1.fread(buf,size,1,fp)一次读入
real    0m0.187s
user    0m0.000s
sys     0m0.180s

2.read(fdin,(void *)buf,size)一次读入
real    0m0.187s
user    0m0.000s
sys     0m0.184s

3.多次fgets(buf,size,fp),每次1k
real    0m0.356s
user    0m0.136s
sys     0m0.220s

4.fgets(buf,size,fp)一次读入
real    0m0.305s
user    0m0.072s
sys     0m0.232s

上 面看到越简单的函数(read()和fread()),速度越快,其他的输入封装函数只不过是为了方便满足特殊需要,并不见得读的速度会很快。对于3和4,因为在sys_read()内部有对读入大小的判断和while循环,所以大文件读取也没必要像3那样分多次调用fgets()来读文件.

另外用mmap()内存映射来读入文件的时间如下:
real    0m0.231s
user    0m0.068s
sys     0m0.164s

也并没有比直接read()快,网上找到一种解释是对于需要频繁读写操作的,mmap效率才会显著提高。下面来模拟频繁读写操作。

二. 文件频繁读写速度测试

1. 这一个测试模拟频繁文件读写操作,500M大小的数据文件data.in,每次从中读入1k个字节对每个字节做加1简单计算,再写到另一个文件data.out中。
//mmapx1.c
#define size 1024*1024*500
#define LEN 1024
#include <stdio.h>
int main()
{
    FILE *fp1,*fp2;
    char *buf=new char[LEN];
    int i,j;
    fp1=fopen("data.in","rb");
    fp2=fopen("data.out","wb");
    for(j=0;j<1024*500;j++)
    {
        fread(buf,1024,1,fp1);
        for(i=0;i<LEN;i++)
            buf[i]++;
        fwrite(buf,LEN,1,fp2);
    }
    printf("ok!/n");
    fclose(fp1);
    fclose(fp2);
}
time 命令测试时间,每次结果都不一样,机器负载有关。5次输出的结果如下:
real 19.592s     18.517s     18.003s     20.470s     20.004s
usr 2.924s       2.964s       3.000s       2.812s       2.972s
sys 2.472s       2.360s       2.344s       2.652s       0.396s

2. 如果采用内存映射,将文件data.in映射到存储区,就避免的对文件的频繁读写操作,而全部转变为I/O存储读写。下面一个程序就是用mmap映射将文件data.in中每个数据加1存储到data.out中。
//mmapx2.c
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define size 1024*1024*500
#define LEN 1024
int main()
{
    int fdin,fdout;
    struct stat statbuf;
    void *src,*dst;
    char *p;
    int i,j;
    fdin=open("data.in",O_RDONLY);
    fdout=open("data.out",O_RDWR|O_CREAT|O_TRUNC);

    if((src=mmap(0,size,PROT_READ,MAP_SHARED,fdin,0))==MAP_FAILED)
    {
        printf("src map error/n");
        return -1;
    }
    lseek(fdout,size-1,SEEK_SET);
    write(fdout,"/0",1);
   //因为data.out是空文件,必须创建一个空洞让其大小成为size来进行下面的mmap

    if((dst=mmap(0,size,PROT_READ|PROT_WRITE,MAP_SHARED,fdout,0))==MAP_FAILED)
    {
        printf("dest map error/n");
        return -1;
    }
        memcpy(dst,src,size);
        p=(char*)dst;
    for(i=0;i<size/LEN;i++)
    {
        for(j=0;j<LEN;j++)
            p[j]++;
        p+=LEN;
    }
    printf("ok/n");
    close(fdout);
    return 0;
}
time测试的5次运行时间如下:
real  9.603s      8.977s      9.416s      9.587s      9.322s
usr  2.764s      2.748s      2.784s      2.840s      2.787s
sys  1.516s      1.384s      1.384s      1.224s      1.276s

结论:上面可以看到,对于频繁IO读写,采用mmap存储映射可以有效提高效率(20s到9s)

3.mmap内存映射还有一个好处,就是直接在一个文件内任意读写修改某个字节,就像操作存储区一样,比如下面一段程序是实现对data.in中每个字节数据加1的计算。

//mmapx3.c
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define size 1024*1024*500
#define LEN 1024
int main()
{
    int fd;
    void *src;
    char *p;
    int i,j;
    fd=open("data.in",O_RDWR);
    if((src=mmap(0,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED)
    {
        printf("src map error/n");
        return -1;
    }
    p=(char*)src;
    for(i=0;i<size/LEN;i++)
    {
        for(j=0;j<LEN;j++)
            p[j]++;
        p+=LEN;
    }
    printf("ok/n");
    close(fd);
}
time测试的5次的运行时间如下:
real 2.820s     2.828s     2.856s     2.818s     2.889s
usr 2.624s     2.624s     2.676s     2.648s     2.584s
sys 0.196s     0.196s     0.176s     0.172s     0.288s

上面对一个大文件内的所有字节各自改变加1,并保存在原来的文件中,采用mmap存储映射的方法可以大大提高效率。对于频繁地随机改写某个文件内的某些部分字节内容的情况来说,这是一个有效的选择。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章