从fread和mmap 谈读文件的性能
生活随笔
收集整理的這篇文章主要介紹了
从fread和mmap 谈读文件的性能
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
[原文] 1.?https://www.byvoid.com/blog/fast-readfile/ 2.?http://blog.csdn.net/jwh_bupt/article/details/7793549 3.?http://blog.csdn.net/jwh_bupt/article/details/8373063
[原文1] ? ? ? ? 在進行大規(guī)模數(shù)據(jù)處理時,讀文件很有可能成為速度瓶頸。不管你的CPU有4個核還是8個核,主頻有2G還是3G,硬盤IO速度總是有個上限的。在本人最近的一次經(jīng)歷中,對一個11G的文本進行數(shù)據(jù)處理,一共耗時34.8秒,其中竟然有30.2秒用在訪問IO上,占了所有時間的87%左右。
? ? ? ? 雖然說硬盤IO是有上限的,那么C++為我們提供的各函數(shù),是否都能讓我們達到這個上限呢?為了求得真相,我對這個11G的文本用fread函數(shù)讀取,在linux下用iostat檢查硬盤的訪問速度,發(fā)現(xiàn)讀的速度大約在380M/s。然后用dd指令測了一下讀文本的訪問速度,發(fā)現(xiàn)速度可以達到460M/s??梢妴尉€程fread訪問并沒有達到硬盤的讀取上限。第一次考慮會不會是fread訪問硬盤的時候有一些固定開銷,用多線程可以達到流水訪問IO的效果提高讀文本的效率,結(jié)果發(fā)現(xiàn)多線程也只有380M/s的讀取速率。
char?*data?=?NULL;?? int?fd=open(“file.txt”,O_RDONLY);??? long?size?=?lseek(fd,?0,?SEEK_END);?? data?=?(char?*)?mmap(?NULL,??size?,PROT_READ,?MAP_PRIVATE,?fd,?0?);???
? ? ? ? 這時file.txt文件中的數(shù)據(jù)就可以從data指針指向的首地址開始訪問了。
[原文2] Linux的IO從廣義上來說包括很多類,從狹義上來說只是講磁盤的IO。在本文中我也就只是主要介紹磁盤的IO。在這里我對Linux的磁盤IO的常用系統(tǒng)調(diào)用進行深入一些的分析,希望在大家在磁盤IO產(chǎn)生瓶頸的時候,能夠幫助做優(yōu)化,同時我也是對之前的一篇博文作總結(jié)。
一、讀磁盤: ssize_t read(int fd,void * buf ,size_t count); 讀磁盤時,最常用的系統(tǒng)調(diào)用就是read(或者fread)。大家都很熟悉它了,首先fopen打開一個文件,同時malloc一段內(nèi)存,最后調(diào)用read函數(shù)將fp指向的文件讀到這段內(nèi)存當中。執(zhí)行完畢后,文件讀寫位置會隨讀取到的字節(jié)移動。 雖然很簡單,也最通用,但是read函數(shù)的執(zhí)行過程有些同學可能不大了解。 這個過程可以總結(jié)為下面這個圖: ? 圖中從上到下的三個位置依次表示: 1. 文件在磁盤中的存儲地址; 2. 內(nèi)核維護的文件的cache(也叫做page cache,4k為一頁,每一頁是一個基本的cache單位); 3. 用戶態(tài)的buffer(read函數(shù)中分配的那段內(nèi)存)。 發(fā)起一次讀請求后,內(nèi)核會先看一下,要讀的文件是否已經(jīng)緩存在內(nèi)核的頁面里面了。如果是,則直接從內(nèi)核的buffer中復制到用戶態(tài)的buffer里面。如果不是,內(nèi)核會發(fā)起一次對文件的IO,讀到內(nèi)核的cache中,然后才會拷貝到buffer中。 這個行為有三個特點: 1. read的行為是一種阻塞的系統(tǒng)調(diào)用(堵在這,直到拿到數(shù)據(jù)為止); 2. 以內(nèi)核為緩沖,從內(nèi)核到用戶內(nèi)存進行了一次內(nèi)存拷貝(而內(nèi)存拷貝是很占用CPU的) 3. 沒有顯示地通知使用者從文件的哪個位置開始去讀。使用者需要利用文件指針,通過lseek之類的系統(tǒng)調(diào)用來指定位置。 這三個特點其實都是有很多缺點的(相信在我的描述下大家也體會到了)。對于第二個特點,可以采用direct IO消除這個內(nèi)核buffer的過程(方法是fopen的時候在標志位上加一個o_direct標志),不過帶來的問題則是無法利用cache,這顯然不是一種很好的解決辦法。所以在很多場景下,直接用read不是很高效的。接下來,我就要依次為大家介紹幾種更高效的系統(tǒng)調(diào)用。
ssize_t pread(intfd, void *buf, size_tcount, off_toffset); pread與read在功能上完全一樣,只是多一個參數(shù):要讀的文件的起始地址。在多線程的情況下,多個線程要同時讀同一個文件的不同地址時,要對文件指針加鎖,影響了性能,而用pread后就不需要加鎖了,使程序更加高效。解決了第三個問題。
ssize_t readahead(int fd, off64_t offset, size_t count); readahead是一種非阻塞的系統(tǒng)調(diào)用,它只要求內(nèi)核把這段數(shù)據(jù)預(yù)讀到內(nèi)核的buffer中,并不等待它執(zhí)行完就返回了。執(zhí)行readahead后再執(zhí)行read的話,由于之前已經(jīng)并行地讓內(nèi)核讀取了數(shù)據(jù)了,這時更多地是直接從內(nèi)核的buffer中直接copy到用戶的buffer里,效率就有所提升了。這樣就解決了read的第一個問題。我們可以看下面這個例子: 1.? while(time < Ncnts) ? { ? read(fd[i], buf[i], lens); ? process(buf[i]); ? //相對較長的處理過程 ? } ?
2.? while(time < Ncnts) ? { ? readahead(fd[i], buf[i], lens); ? } ? while(time < Ncnts) ? { ? read(fd[i], buf[i], lens); ? process(buf[i]); ? //相對較長的處理過程 ? }
修改后加了readahead之后,readahead很快地發(fā)送了讀取消息后就返回了,而process的過程中,readahead使內(nèi)核并行地讀取磁盤,下一次process的時候數(shù)據(jù)已經(jīng)讀取到內(nèi)核中了,減少了等待read的過程。 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 剛才提到的兩個函數(shù)中,從內(nèi)核的buffer到用戶的buffer這個拷貝過程依然沒有處理。剛才說了,這個內(nèi)存拷貝的過程是很占cpu的。為了解決這個問題,一種方法就是使用mmap(在之前的這篇博文里我已經(jīng)介紹了如何去使用)、它直接把內(nèi)核態(tài)的地址map到用戶態(tài)上,用戶態(tài)通過這個指針可以直接訪問(相當于從磁盤跳過cache直接到用戶態(tài))。 但是mmap同時存在著兩個重要的問題: 1. cache命中率不如read; 2. 在線程模型下互斥十分嚴重 對于第一個問題的解釋,和內(nèi)核的實現(xiàn)機制比較相關(guān)。在實際命中cache的時候,調(diào)用mmap是沒有進行從用戶態(tài)到內(nèi)核態(tài)的切換的,這樣采用LRU更新策略的內(nèi)核頁面沒法讓這個被訪問的頁面的熱度加1,也就是盡管可能這個頁面通過mmap訪問了許多次,但是都沒有被內(nèi)核記錄下來,就好像一次也沒有訪問到一樣,這樣LRU很容易地就把它更新掉了。而read一定陷入到內(nèi)核態(tài)了。為了解決這個問題,可以用一個readahead發(fā)起這次內(nèi)核態(tài)的切換(同時提前發(fā)起IO)。 對于第二個問題產(chǎn)生的原因,則是在不命中內(nèi)核cache的情況下內(nèi)核從disk讀數(shù)據(jù)是要加一把mm級別的鎖(內(nèi)核幫你加的)。加著這個級別的鎖進行IO操作,肯定不是很高效的。readahead可以一定程度解決這個問題,但是更好的辦法是利用一種和readahead一樣但是是阻塞型的系統(tǒng)調(diào)用(具體我不知道Linux是否提供了這樣一種函數(shù))。阻塞的方式讓內(nèi)核預(yù)讀磁盤,這樣能夠保證mmap的時候要讀的數(shù)據(jù)肯定在內(nèi)核中,盡可能地避免了這把鎖。
二、寫磁盤 對比read的系統(tǒng)調(diào)用,write更多是一種非阻塞的調(diào)用方式。即一般是write到內(nèi)存中就可以返回了。具體的過程如下所示: ? 其中有兩個過程會觸發(fā)寫磁盤: 1)dirty ration(默認40%)超過閾值:此時write會被阻塞,開始同步寫臟頁,直到比例降下去以后才繼續(xù)write。
2)dirty background ration(默認10%)超過閾值:此時write不被阻塞,會被返回,不過返回之前會喚醒后臺進程pdflush刷臟頁。它的行為是有臟頁就開始刷(不一定是正在write的臟頁)。對于低于10%什么時候刷臟頁呢?內(nèi)核的后臺有一個線程pdflush,它會周期性地刷臟頁。這個周期在內(nèi)核中默認是5秒鐘(即每5秒鐘喚醒一次)。它會掃描所有的臟頁,然后找到最老的臟頁變臟的時間超過dirty_expire_centisecs(默認為30秒),達到這個時間的就刷下去(這個過程與剛才的那個后臺進程是有些不一樣的)。 寫磁盤遇到的問題一般是,在內(nèi)核寫磁盤的過程中,如果這個比例不合適,可能會突然地寫磁盤占用的IO過大,這樣導致讀磁盤的性能下降。
[參考]1.?https://www.byvoid.com/blog/fast-readfile/ 2.?http://blog.csdn.net/jwh_bupt/article/details/7793549 3.?http://blog.csdn.net/jwh_bupt/article/details/8373063
[原文1] ? ? ? ? 在進行大規(guī)模數(shù)據(jù)處理時,讀文件很有可能成為速度瓶頸。不管你的CPU有4個核還是8個核,主頻有2G還是3G,硬盤IO速度總是有個上限的。在本人最近的一次經(jīng)歷中,對一個11G的文本進行數(shù)據(jù)處理,一共耗時34.8秒,其中竟然有30.2秒用在訪問IO上,占了所有時間的87%左右。
? ? ? ? 雖然說硬盤IO是有上限的,那么C++為我們提供的各函數(shù),是否都能讓我們達到這個上限呢?為了求得真相,我對這個11G的文本用fread函數(shù)讀取,在linux下用iostat檢查硬盤的訪問速度,發(fā)現(xiàn)讀的速度大約在380M/s。然后用dd指令測了一下讀文本的訪問速度,發(fā)現(xiàn)速度可以達到460M/s??梢妴尉€程fread訪問并沒有達到硬盤的讀取上限。第一次考慮會不會是fread訪問硬盤的時候有一些固定開銷,用多線程可以達到流水訪問IO的效果提高讀文本的效率,結(jié)果發(fā)現(xiàn)多線程也只有380M/s的讀取速率。
? ? ? ? 為什么fread的效率達不到最大呢?查閱一些資料才知,用fread/fwrite方式訪問硬盤,用戶須向內(nèi)核指定要讀多少,內(nèi)核再把得到的內(nèi)容從內(nèi)核緩沖池拷向用戶空間;寫也須要有一個大致如此的過程。這樣在訪問IO的時候就多經(jīng)歷了這么一個內(nèi)核的buffer,造成速度的限制。一個解決的辦法是mmap。mmap就是通過把文件的某一塊內(nèi)容直接映射到用戶空間上,用戶可以直接向內(nèi)核緩沖池讀寫這一塊內(nèi)容,這樣一來就少了內(nèi)核與用戶空間的來回拷貝所以通常更快。
?
? ? ? ? mmap的使用方法如下:
[cpp]?view plaincopyprint?
? ? ? ? 這時file.txt文件中的數(shù)據(jù)就可以從data指針指向的首地址開始訪問了。
? ? ? ? 為了從數(shù)據(jù)說明這個問題,我引用一位網(wǎng)友的結(jié)論,希望對大家有所啟發(fā)。
| 方法/平臺/時間(秒) | 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 |
[原文2] Linux的IO從廣義上來說包括很多類,從狹義上來說只是講磁盤的IO。在本文中我也就只是主要介紹磁盤的IO。在這里我對Linux的磁盤IO的常用系統(tǒng)調(diào)用進行深入一些的分析,希望在大家在磁盤IO產(chǎn)生瓶頸的時候,能夠幫助做優(yōu)化,同時我也是對之前的一篇博文作總結(jié)。
一、讀磁盤: ssize_t read(int fd,void * buf ,size_t count); 讀磁盤時,最常用的系統(tǒng)調(diào)用就是read(或者fread)。大家都很熟悉它了,首先fopen打開一個文件,同時malloc一段內(nèi)存,最后調(diào)用read函數(shù)將fp指向的文件讀到這段內(nèi)存當中。執(zhí)行完畢后,文件讀寫位置會隨讀取到的字節(jié)移動。 雖然很簡單,也最通用,但是read函數(shù)的執(zhí)行過程有些同學可能不大了解。 這個過程可以總結(jié)為下面這個圖: ? 圖中從上到下的三個位置依次表示: 1. 文件在磁盤中的存儲地址; 2. 內(nèi)核維護的文件的cache(也叫做page cache,4k為一頁,每一頁是一個基本的cache單位); 3. 用戶態(tài)的buffer(read函數(shù)中分配的那段內(nèi)存)。 發(fā)起一次讀請求后,內(nèi)核會先看一下,要讀的文件是否已經(jīng)緩存在內(nèi)核的頁面里面了。如果是,則直接從內(nèi)核的buffer中復制到用戶態(tài)的buffer里面。如果不是,內(nèi)核會發(fā)起一次對文件的IO,讀到內(nèi)核的cache中,然后才會拷貝到buffer中。 這個行為有三個特點: 1. read的行為是一種阻塞的系統(tǒng)調(diào)用(堵在這,直到拿到數(shù)據(jù)為止); 2. 以內(nèi)核為緩沖,從內(nèi)核到用戶內(nèi)存進行了一次內(nèi)存拷貝(而內(nèi)存拷貝是很占用CPU的) 3. 沒有顯示地通知使用者從文件的哪個位置開始去讀。使用者需要利用文件指針,通過lseek之類的系統(tǒng)調(diào)用來指定位置。 這三個特點其實都是有很多缺點的(相信在我的描述下大家也體會到了)。對于第二個特點,可以采用direct IO消除這個內(nèi)核buffer的過程(方法是fopen的時候在標志位上加一個o_direct標志),不過帶來的問題則是無法利用cache,這顯然不是一種很好的解決辦法。所以在很多場景下,直接用read不是很高效的。接下來,我就要依次為大家介紹幾種更高效的系統(tǒng)調(diào)用。
ssize_t pread(intfd, void *buf, size_tcount, off_toffset); pread與read在功能上完全一樣,只是多一個參數(shù):要讀的文件的起始地址。在多線程的情況下,多個線程要同時讀同一個文件的不同地址時,要對文件指針加鎖,影響了性能,而用pread后就不需要加鎖了,使程序更加高效。解決了第三個問題。
ssize_t readahead(int fd, off64_t offset, size_t count); readahead是一種非阻塞的系統(tǒng)調(diào)用,它只要求內(nèi)核把這段數(shù)據(jù)預(yù)讀到內(nèi)核的buffer中,并不等待它執(zhí)行完就返回了。執(zhí)行readahead后再執(zhí)行read的話,由于之前已經(jīng)并行地讓內(nèi)核讀取了數(shù)據(jù)了,這時更多地是直接從內(nèi)核的buffer中直接copy到用戶的buffer里,效率就有所提升了。這樣就解決了read的第一個問題。我們可以看下面這個例子: 1.? while(time < Ncnts) ? { ? read(fd[i], buf[i], lens); ? process(buf[i]); ? //相對較長的處理過程 ? } ?
2.? while(time < Ncnts) ? { ? readahead(fd[i], buf[i], lens); ? } ? while(time < Ncnts) ? { ? read(fd[i], buf[i], lens); ? process(buf[i]); ? //相對較長的處理過程 ? }
修改后加了readahead之后,readahead很快地發(fā)送了讀取消息后就返回了,而process的過程中,readahead使內(nèi)核并行地讀取磁盤,下一次process的時候數(shù)據(jù)已經(jīng)讀取到內(nèi)核中了,減少了等待read的過程。 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 剛才提到的兩個函數(shù)中,從內(nèi)核的buffer到用戶的buffer這個拷貝過程依然沒有處理。剛才說了,這個內(nèi)存拷貝的過程是很占cpu的。為了解決這個問題,一種方法就是使用mmap(在之前的這篇博文里我已經(jīng)介紹了如何去使用)、它直接把內(nèi)核態(tài)的地址map到用戶態(tài)上,用戶態(tài)通過這個指針可以直接訪問(相當于從磁盤跳過cache直接到用戶態(tài))。 但是mmap同時存在著兩個重要的問題: 1. cache命中率不如read; 2. 在線程模型下互斥十分嚴重 對于第一個問題的解釋,和內(nèi)核的實現(xiàn)機制比較相關(guān)。在實際命中cache的時候,調(diào)用mmap是沒有進行從用戶態(tài)到內(nèi)核態(tài)的切換的,這樣采用LRU更新策略的內(nèi)核頁面沒法讓這個被訪問的頁面的熱度加1,也就是盡管可能這個頁面通過mmap訪問了許多次,但是都沒有被內(nèi)核記錄下來,就好像一次也沒有訪問到一樣,這樣LRU很容易地就把它更新掉了。而read一定陷入到內(nèi)核態(tài)了。為了解決這個問題,可以用一個readahead發(fā)起這次內(nèi)核態(tài)的切換(同時提前發(fā)起IO)。 對于第二個問題產(chǎn)生的原因,則是在不命中內(nèi)核cache的情況下內(nèi)核從disk讀數(shù)據(jù)是要加一把mm級別的鎖(內(nèi)核幫你加的)。加著這個級別的鎖進行IO操作,肯定不是很高效的。readahead可以一定程度解決這個問題,但是更好的辦法是利用一種和readahead一樣但是是阻塞型的系統(tǒng)調(diào)用(具體我不知道Linux是否提供了這樣一種函數(shù))。阻塞的方式讓內(nèi)核預(yù)讀磁盤,這樣能夠保證mmap的時候要讀的數(shù)據(jù)肯定在內(nèi)核中,盡可能地避免了這把鎖。
二、寫磁盤 對比read的系統(tǒng)調(diào)用,write更多是一種非阻塞的調(diào)用方式。即一般是write到內(nèi)存中就可以返回了。具體的過程如下所示: ? 其中有兩個過程會觸發(fā)寫磁盤: 1)dirty ration(默認40%)超過閾值:此時write會被阻塞,開始同步寫臟頁,直到比例降下去以后才繼續(xù)write。
2)dirty background ration(默認10%)超過閾值:此時write不被阻塞,會被返回,不過返回之前會喚醒后臺進程pdflush刷臟頁。它的行為是有臟頁就開始刷(不一定是正在write的臟頁)。對于低于10%什么時候刷臟頁呢?內(nèi)核的后臺有一個線程pdflush,它會周期性地刷臟頁。這個周期在內(nèi)核中默認是5秒鐘(即每5秒鐘喚醒一次)。它會掃描所有的臟頁,然后找到最老的臟頁變臟的時間超過dirty_expire_centisecs(默認為30秒),達到這個時間的就刷下去(這個過程與剛才的那個后臺進程是有些不一樣的)。 寫磁盤遇到的問題一般是,在內(nèi)核寫磁盤的過程中,如果這個比例不合適,可能會突然地寫磁盤占用的IO過大,這樣導致讀磁盤的性能下降。
[參考]1.?https://www.byvoid.com/blog/fast-readfile/ 2.?http://blog.csdn.net/jwh_bupt/article/details/7793549 3.?http://blog.csdn.net/jwh_bupt/article/details/8373063
總結(jié)
以上是生活随笔為你收集整理的从fread和mmap 谈读文件的性能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CLion 2016.1新增Python
- 下一篇: ECMAScript6 新特性——“字符