原文地址:
http://student.csdn.net/space.php?uid=112600&do=blog&id=14316
部門最近在搞JVM上的動態語言,比如Groovy。在享受了動態語言的種種靈活之後,效能自然而然被拿出來PK。
然後玩Python的同事就舊事重提,從網上找來一段Python代碼,很多Python的人都知道了,很多C++的人也知道了,它跑得很快。
為了讓文章好看一點,我來編一個故事,說,有個軟體公司,正要招剛從大學畢業的C++程式員和Python程式,面試題是同一道:
有一個15萬行的文檔,其中很多行的內容相同的,請寫一段代碼,讀入這檔案,儘可能快地將不重複的行內容輸出到新檔案,注意次序不要改變”,比如,有5行內容:
B1
B2
A2
B2
B1
要求新檔案內容為:
B1
B2
A2
小P新學習Pythont不久,他寫出代碼如下:
Code:
- import time
- if __name__ == "__main__" :
- beg = time.time()
- hashtable = {}
- fi = file("test.txt", "r")
- fo = file("test_dst.txt", "w")
-
- count = 0;
- for line in fi :
- if not hashtable.has_key(line) :
- hashtable[line] = 1
- fo.write(line)
- count = count +1
-
- fo.close()
- fi.close()
-
- end = time.time()
-
- print "count=", count, "time=", (end - beg)*1000, "ms"
這是一段中規中矩的Pythont程式,符合Python的風格:看不出是新手還是老手寫的。:)
OK,在我的機器上,我準備了一個15萬文字,但不重複行只有4萬行的檔案,上面的代碼已耗用時間是281 毫秒。效能比一會兒的相同邏輯的C++代碼還要快,所以小P被相中了。
接來是C++的面試。 嗯,不懂 hash的同學可能要被拋棄了,用vector,list的都會非常非常的慢,或許用map的人可以考慮,理由是嚴格地講STL裡是沒有hash資料結構的。但反過來說,有在學校裡認真學習資料結構,並且有實際使用STL的C++程式員,應該都知道幾乎所有版本的STL實作,都有附加實現了hash——再者,還是從效能上,用map也還是很慢很慢,你應該知道在find上,它的複雜度和hash表是什麼倍數。
還好,故事中的小C,他雖然學習C++也不久,但他懂hash,也寫了一段中規中矩的代碼:
Code:
- #include <iostream>
- #include <string>
- #include <fstream>
- #include <sstream>
- #include <ctime> //for clock()
- #include <ext/hash_map>
-
- using namespace std;
- using namespace __gnu_cxx;
-
- struct string_hash
- {
- size_t operator()(const string& str) const
- {
- return __stl_hash_string(str.c_str());
- }
- };
-
- struct string_compare
- {
- bool operator () (string const& str1, string const& str2)
- {
- return str1 == str2;
- }
- };
-
- int main()
- {
- clock_t beg = clock();
-
- hash_map<string, int, string_hash, string_compare> hastable;
-
- ifstream fi ("test.txt");
- ofstream fo ("test_dst.txt");
-
- string line;
- size_t count = 0;
-
- for (getline(fi, line); !fi.eof(); getline(fi, line))
- {
- if (hastable.find(line) == hastable.end())
- {
- hastable[line] = 1;
- fo << line << endl;
- ++count;
- }
- }
-
- fo.close();
- fi.close();
-
- clock_t end = clock();
- cout << "count = " << count << ", time = " << (end-beg) << "ms" << endl;
-
- return 0;
- }
寫的代碼和python版本相比,似乎長了點,但其實邏輯一致,得分應該和小P差不多,但我們現在,正在掙紮,要不要小C呢!為什麼?因為同樣作為初學者,同樣一道題,作為一個C++的程式員,他寫這道題,效能比Python那個版本,要來得差呢!我們把他的代碼編譯為速度最佳化版本,相同測試條件,連續跑了10次,最慢的要1秒多,最快也就600毫秒。遠遠比不上Python那個版本的281毫秒。如果要說最佳化,我們知道,用上PSYCO,代碼基本上一行不改,就可再提速近100毫秒……
要拒掉小C,說他什麼好?有人說,怪他不懂得使用API的優勢,可以直接調用Windows API 的檔案對應等技術,也可以不使用__stl_hash_string 這個由STL提供的字串產生hash值的演算法(埋怨人家小C數學學得不太好?);還可以埋怨他偷懶使用C++擴充庫hash容器,其實只要1千行左右吧,應該可以寫得了同個更高插入的尋找效能的雜湊表……(見此)
是的,這就是C++語言,我很不喜歡的某種“哲學”,高手總是可以有各種牛逼哄哄的手段,來寫出相當不一般的,相當可以和初學者拉開令人“敬佩”的距離……
這可不是我喜歡的! 如果C++語言的學習,就是非得把一個初學者變成一個專家(,然後他才算入了門) 就已經夠雷人了,那在庫的使用上,我真不想逼著每個人都精通這個演算法那個演算法,都精通無數個庫,都精通Windows或Linux那無數個API——哈哈,其這我也不是真正的教育家,這樣說得有些沉重了。
真正的事實通常是這樣,我,一個主要用C++的程式員,和我的那位玩Python的同事(他在工作中基本上不編程),下班後喝點小酒,然後扯到這件效能PK的事來,你說,我出手拿出一段數百,甚至上千行的代碼,去PK人家的簡潔優雅的20行代碼。我這哪好意思嘛,簡直就是未經先亂了陣腳。再者,人家的代碼拿到linux下一斷行符號,就能執行,你說我好意思還要在千行代碼,非常wei suo地藏了幾行Windows的API調用嗎?
輸就輸了,但還是要在可接受的範圍內,做點最佳化——Python俺也不是完全不懂,動態語言的一些內幕,我也略知一二的說。呵呵。這個可接受的範圍,我約定的是,一不調用特定的作業系統的API,二是在C++環境內搞定。
Code:
- int main()
- {
- clock_t beg = clock();
-
- //最佳化一:給hastable預設一個足夠大的數目
- hash_map<string, int, string_hash, string_compare> hastable(40050);
-
- ifstream fi ("test.txt");
- istringstream ssi;
-
- fi.seekg(0, ifstream::end);
- size_t fileSize = fi.tellg();
- fi.seekg(0);
-
- //最佳化二:一次讀入檔案的全部內容到記憶體
- char* buf = new char[fileSize];
- fi.read(buf, fileSize);
-
- //最佳化三:從存有資料的那段記憶體,直接構造出"記憶體流", 避免複製
- ssi.rdbuf()->pubsetbuf(buf, fi.gcount());
- fi.close();
-
- string line;
- size_t count = 0;
-
- ostringstream sso;
-
- //最佳化四:從記憶體流讀每一行,而不是從檔案流
- for(getline(ssi, line); !ssi.eof(); getline(ssi, line))
- {
- if (hastable.find(line) == hastable.end())
- {
- hastable[line] = 1;
- //最佳化五:先輸出到記憶體流,而不是到檔案流
- sso << line << endl;
- ++count;
- }
- }
- delete [] buf;
-
- ofstream fo ("test_dst.txt");
- fo << sso.str() << endl;
- fo.close();
-
- clock_t end = clock();
- cout << "count = " << count << ", time = " << (end - beg) << "ms" << endl;
-
- return 0;
- }
取檔案大小,沒有考慮超過2G的檔案,我想在本“面試”中,是可以接受的。另一處要推敲的是,clock_t在linux下,會大很多,可以通過一個CLOCK_PER_SEC(?)宏解決,這裡略。
1. 預定hash表個數
雜湊表上來就先預定4萬多個(差不多就是受測檔案內容不同的行數),這有點賴皮呵呵。(或許你的hash沒這功能)
2.一次讀入檔案全部內容
一次性讀入檔案,這可以理直氣壯,估計py的C語言實現,也會玩這個。
3. 將記憶體直接變成"流"
讀入記憶體後,通過“pubsetbuf” 函數,直接將它變成一個流(stringstream),這也是必然的,可以避免一次大記憶體申請,更主要是是記憶體複製。我相信對於多數語言,甚至同樣是靜態Java語言,這都是此類需求的必然的實現方法。
4. 從記憶體流讀,而不是從檔案流
這也是第3步的目標。
5. 先輸出到記憶體,最後再一次輸出成檔案。
這應該是最先想到的,而且效果可能也是最好的一個。
----------------------------------
就這樣,還是比python慢:最快時是296秒。幾個千分之一秒的差距嘛,呵呵……同事要祭出Psyco了。
我該怎麼辦呢?沒事,我可以說,輸贏不是目的,互相學習,理解不同語言間的機制,才是重要的——有點官腔的說——這樣說吧,我的C++水平很一般……會有人有更漂亮而優雅的最佳化的。
-補充:-----------最後的最佳化:升級編譯器 ----------------------------------------------------------
下面評論中有網友說,他只是最佳化了讀寫IO,用的是tr1(Technical Report 1,有待以後正式加入)下的hash_map,速度很是快了些了。非常感謝他的提醒,我們不妨試試同樣是簡單的(我不準備在這個小遊戲上,大動幹戈地改代碼),但有效效能最佳化之最後招數:我的Python同事有Psyco不可怕,我們坐著就可以享受編譯器的升級。
我想起我用的編譯器是非常古老mingw32-gcc3.4.5,費點勁,從網上下載了新版裝上(4.4.0,好像也不是最新的gcc)。
一行代碼不改,從296,直接提升為187 好,現在C++完勝未最佳化版本的Python代碼,可以和Psyco的版本比比了。
--------------------------------------------------------------------
最主要的是,效能和優雅,有時候後者更重要,不然的話,我也不會跟著玩了python這麼些年,它效能差的地方,可多了。但它真的優雅。直到我發現,其實 除非逼急了,否則C++ 你也可以一直用得很優雅。而且優雅不是個人的事,當一個團隊的人都保持優雅了,事情就美妙了。
換一句通俗的原則:千萬別一開始就想著效能最佳化的事,所以,小C我們要了。
PS:
hash_map在vs下使用和在gcc下使用是有差異的,如下:
1.vs中:
#include <hash_map>
// 注意標頭檔和namespace
using namespace stdext;
int main()
{
hash_map<string, int> hmap;
return 0;
}
2.gcc下:
#include <ext/hash_map>
using namespace __gnu_cxx;
// 需要自己寫hash函數
struct string_hash
{
size_t operator()(const string& str) const
{
return __stl_hash_string(str.c_str());
}
};
int main()
{
hash_map<string, int, string_hash> hmap;
return 0;
}
另外,在vs中使用hash_map還要特別注意待雜湊對象的hash函數和comp函數的定義形式。
舉個例子,對於我的自訂類型
typedef struct StatusNode
{
int data[3];
StatusNode* next;
StatusNode* parent;
}StatusNode,StatusList;
在gcc下,我們對StatusNode作hash所作的工作如下:
struct SN_hash
{
size_t operator()(const StatusNode& sn) const
{
return sn.data[0]+sn.data[1]*100+sn.data[2]*10000;
};
};
struct SN_comp
{
bool operator()(const StatusNode& sn1,const StatusNode& sn2) const
{
return (sn1.data[0]+sn1.data[1]*100+sn1.data[2]*10000)==(sn2.data[0]+sn2.data[1]*100+sn2.data[2]*10000);
};
};
然後
hash_map<StatusNode, int, SN_hash, SN_comp> hmap;
即可正常使用
但在vs下,你會發現這種作法是行不通的,取而代之的方法如下:(hash函數和comp函數定義在同一個struct中)
struct SN_hashmap
{
static const size_t bucket_size = 4;
static const size_t min_buckets = 8;
size_t operator()(const StatusNode& sn) const
{
return size_t(sn.data[0]+sn.data[1]*100+sn.data[2]*10000);
}
bool operator()(const StatusNode& sn1,const StatusNode& sn2) const
{
return (sn1.data[0]+sn1.data[1]*100+sn1.data[2]*10000)==(sn2.data[0]+sn2.data[1]*100+sn2.data[2]*10000);
}
};
然後
hash_map<StatusNode, int, SN_hashmap> hmap;
使用