題目爲:兩個200G大小的文件A和B,AB文件裏內容均爲無序的一行一個正整數字(不超過2^63),請設計方案,輸出兩個文件中均出現過的數字,使用一臺內存不超過16G、磁盤充足的機器。方案中指明使用java編程時使用到的關鍵工具類,以及爲什麼?
對於這種大數據量問題(至少對於一臺機器來說算是大數據了),使用MapReduce是最簡單的方式了。現在開源的最好的支持MapReduce的分佈式計算框架軟件就是Hadoop,而Hadoop是用Java寫的,整個運行時系統也是需要Java虛擬機的支持。所以這個問題放在了Java組,當然這只是我的猜測。
改日我再把MapReduce的代碼和實驗結果發上來,今天主要不是討論MapReduce。而是討論使用共享內存的編程模型解決這個問題的辦法。共享內存的並行編程模型現在比較常用的是多線程,GPU,還有OpenMP等。這裏使用多線程的方式來解決這個問題。
以下分析對文件的數據存儲重新做一個假設,假設數據都是以二進制的形式存儲,每個元素的數據類型爲uint64_t,即佔8個字節,所以一共有25G個uint64_t類型的元素。
首先從數據結構的角度去考慮這個問題,對於一般的尋找兩個列表listA和listB中相同元素的問題,我們可以使用的最簡單的算法如下:
- foreach itemA in listA
- foreach itemB in listB
- if(itemB == itemA)
- print itemA;
- break;
- end if;
- end foreach;
- end foreach;
設len(listA)爲M,len(listB)爲N。算法的時間複雜度爲O(MN)。這裏假設M=N,則時間複雜度爲O(M^2)。對於200G的文件,就意味着對於A文件的每一個數,都需要對B文件掃描一次。假設磁盤的讀取速度爲50MB/s,需要的時間爲:25G*200G/50MB/s =~ 10^14s =~ 3170979 年。也就是說如果用這種方法的話,大致需要300萬年。
當然沒有人會願意這麼做的,除非N的規模很小的數據才考慮這麼做。學過點數據結構的人都知道應該用二叉平衡樹,二叉查找樹,B+,B-之類的樹形結構來存儲數據。這樣對第一個文件首先建立樹的時間複雜度爲NlogN,對第二個文件中的每個元素,在樹中查找是否有相同的數,查找複雜度爲logN。對所有元素查找的時間複雜度就爲NlogN。所以總時間複雜度爲2NlogN,即:O(NlogN)。使用Big O這種時間複雜度分析方法一般是針對於數據都能夠放入內存的算法。對於這個問題,這樣分析就太樂觀了。首先,建立平衡樹的時候,內存會爆掉,而且可以爆掉幾十次了。就算是用硬盤索引技術,把一部分樹放在磁盤上,這樣在查找的時候會更加痛苦。因爲要根據樹的查找路徑不斷的移動磁頭需找新的索引位置,對於大規模數據,你要想不停的移動磁頭,那你就輸了。
還有一種方式就是借鑑MapReduce這種流處理方式,對兩個文件的數據進行排序,然後再對比是否有相同的數據。使用Big O的分析這個算法的時間複雜度,應該爲:NlogN+NlogN+N+N.所以時間複雜度爲:O(NlogN).這樣看起來和使用樹形數據結構的效果差不多。但是,這有一個顯著的優點就是你的數據可以從磁盤上“大塊大塊連續連續”的讀取,磁頭不需要跳動,這對於這種對上世紀大機械時代有懷舊感的老技術還是非常划得來的。
所以大致的處理方式如下,首先對200G文件A進行分塊排序,排序後每塊放入一個文件中,然後對文件B做同樣的處理。文件A對應的分塊有序文件分別爲A1,A2,A3,…,AN(如圖1所示)。文件B對應的分塊有序文件分別爲B1,B2,B3,…,BN(如圖2所示)。然後使用文件指針分別指向A1,A2,A3,…AN和B1,B2,B3,…BN。接下來的方法就很類似於歸併排序的方式了,從A1到AN所指向的文件中取出一個最小minA,然後從B1-BN所指向的文件中也取出一個最小值minB,判斷兩者是否相等,若minA==minB,則將這個值寫入到結果文件(指針fpResult所指向的文件),然後兩者指針都向後移動,minA>minB.則從取出minB對應的那個文件指針向後移動一個單元,接着再從B1-BN中輸出一個最小值。minA<minB的處理方式也是一樣的,如圖3所示。
圖1 文件A分塊排序
圖2 文件B分塊排序
圖3 歸併尋找相同值
歸併的算法可以使用錦標賽算法,而不是用普通的在N個元素中尋找最小值的算法。
接下來詳細分析下,對每一塊分塊排序的方法,假設塊的大小爲1G,則需要以下三個步驟:
1.將1G數據從磁盤中載入內存
2.對1G數據進行排序
3.將排序後的結果寫入磁盤
首先我們需要一個計時程序,用來統計程序各個模塊運行時所耗費的時間,這個程序由timer.h和timer.c構成:
/*timer.h*/
- /*
- * Author: Chaos Lee
- * Date: 2012-06-30
- * Description: interfaces for public to use timer
- */
- #ifndef __TIMER_H_
- #define __TIMER_H_
- #include<stdio.h>
- #include<sys/time.h>
- void start_timer();
- int get_elapsed_time();
- #endif
/*timer.c*/
- /*
- * Author: Chaos Lee
- * Date: 2012-06-30
- * Description: implementation of the timer
- */
- #include "timer.h"
- static struct timeval start,end;
- void start_timer()
- {
- gettimeofday(&start,NULL);
- }
- void restart_timer()
- {
- start_timer();
- }
- int get_elapsed_time()
- {
- gettimeofday(&end,NULL);
- return end.tv_sec-start.tv_sec + (end.tv_usec-start.tv_usec)/1000000;
- }
其次,我們還需要一個程序用來產生隨機數文件,以下該程序的源代碼,使用的時候需要傳入一個參數N,則產生大小爲2^N 字節的文件。
/*random_generator.c*/
- /*
- * Author: Chaos Lee
- * Date: 2012-06-30
- * Description: Generating a file containing a given number of random elements whose type are uint64_t
- */
- #include<stdio.h>
- #include<stdlib.h>
- #include<time.h>
- #include<stdint.h>
- #include<sys/time.h>
- #include "timer.h"
- int main(int argc,char *argv[])
- {
- int shift,tmp[2];
- FILE * fp;
- int64_t size,i;
- int elapsed_seconds;
- start_timer();
- if(2 > argc)
- {
- fprintf(stderr,"Usage:%s NUMBER\n",argv[0]);
- exit(1);
- }
- shift = atoi(argv[1]);
- shift -= 3;
- if(0 > shift)
- {
- fprintf(stderr,"too small\n");
- exit(1);
- }
- size = 1 << shift;
- srand(time(NULL));
- fp = fopen("data.dat","wb");
- if(NULL == fp)
- {
- fprintf(stderr,"file open error.");
- exit(1);
- }
- for(i=0;i<size;i++)
- {
- tmp[1] = rand();
- tmp[2] = rand();
- if( 2 != fwrite(&tmp[0],sizeof(int),2,fp))
- {
- fprintf(stderr,"writing file failure...\n");
- exit(1);
- }
- }
- elapsed_seconds = get_elapsed_time();
- fprintf(stdout,"generating cost %d seconds.\n",elapsed_seconds);
- fclose(fp);
- }
對於數據排序可以使用單線程的排序方法或者多線程的排序方法。單線程的排序版本的源代碼如下:
/* single_thread_sort.c */
- /*
- * Author: Chaos Lee
- * Date: 2012-06-30
- * Description: load,sort,store data with single core
- */
- #include<stdio.h>
- #include<stdlib.h>
- #include<stdint.h>
- #include<sys/types.h>
- #include<sys/stat.h>
- #include "../error.h"
- #include "timer.h"
- int uint64_compare(const void * ptr1,const void * ptr2)
- {
- return *((uint64_t *)ptr1) > *((uint64_t *)ptr2) ? 1 : *((uint64_t *)ptr1) < *((uint64_t *)ptr2) ? -1 : 0;
- }
- int main(int argc,char * argv[])
- {
- struct stat data_stat;
- int status,elapsed_seconds;;
- uint64_t size;
- uint64_t *buffer;
- FILE * fp;
- FILE * fp_result;
- status = stat("data.dat",&data_stat);
- if(0 != status)
- error_abort("stat file error.\n");
- size = data_stat.st_size;
- buffer = (uint64_t *) malloc(size);
- if(NULL == buffer)
- {
- fprintf(stderr,"mallocing error.");
- exit(1);
- }
- fp = fopen("data.dat","rb");
- if(NULL == fp)
- {
- fprintf(stderr,"file open error.");
- exit(1);
- }
- start_timer();
- fread(buffer,size,1,fp);
- elapsed_seconds = get_elapsed_time();
- fprintf(stdout,"loading cost %d seconds\n",elapsed_seconds);
- restart_timer();
- qsort(buffer,size/sizeof(uint64_t),sizeof(uint64_t),uint64_compare);
- elapsed_seconds = get_elapsed_time();
- fprintf(stdout,"sorting cost %d seconds\n",elapsed_seconds);
- fp_result = fopen("single_result.dat","wb");
- if(NULL == fp_result)
- {
- fprintf(stderr,"open result file error.\n");
- exit(1);
- }
- restart_timer();
- fwrite(buffer,sizeof(uint64_t),size/sizeof(uint64_t),fp_result);
- elapsed_seconds = get_elapsed_time();
- fprintf(stdout,"writing results cost %d seconds\n",elapsed_seconds);
- free(buffer);
- fclose(fp);
- return 0;
- }
單線程版本的運行時間和測試方法如下:
- [lichao@sg01 thread_power]$ gcc -c timer.c -o timer.o
- [lichao@sg01 thread_power]$ gcc random_generator.c -o random_generator timer.o
- [lichao@sg01 thread_power]$ ./random_generator 30
- generating cost 36 seconds.
由此可見,創建1GB的文件耗時36秒,即寫的速度爲: 29826161B/s.差不多爲30MB/s。下面編譯單線程版本的代碼,並運行測試下時間:
- [lichao@sg01 thread_power]$ gcc single_thread_sort.c -o single_thread_sort timer.o -lpthread
- [lichao@sg01 thread_power]$ ./single_thread_sort
- loading cost 44 seconds
- sorting cost 85 seconds
- writing results cost 81 seconds
圖4 排序階段CPU使用率
由於篇幅所限,接下來的內容請看下一篇博文:百度實習生招聘的一道大數據處理題目(下)