探索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存儲映射的方法可以大大提高效率。對於頻繁地隨機改寫某個文件內的某些部分字節內容的情況來說,這是一個有效的選擇。

 

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