[zz]c++有時比Python慢

來源:互聯網
上載者:User

原文地址:

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:
  1. import time  
  2. if __name__ == "__main__" :  
  3.     beg = time.time()  
  4.     hashtable = {}  
  5.     fi = file("test.txt", "r")  
  6.     fo = file("test_dst.txt", "w")  
  7.   
  8.     count = 0;  
  9.     for line in fi :  
  10.         if not hashtable.has_key(line) :  
  11.             hashtable[line] = 1  
  12.             fo.write(line)  
  13.             count = count +1  
  14.   
  15.     fo.close()  
  16.     fi.close()  
  17.   
  18.     end = time.time()  
  19.       
  20.     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:
  1. #include <iostream>  
  2. #include <string>  
  3. #include <fstream>  
  4. #include <sstream>  
  5. #include <ctime> //for clock()  
  6. #include <ext/hash_map>  
  7.   
  8. using namespace std;  
  9. using namespace __gnu_cxx;  
  10.   
  11. struct string_hash  
  12. {  
  13.     size_t operator()(const string& str) const  
  14.     {  
  15.         return __stl_hash_string(str.c_str());  
  16.     }  
  17. };  
  18.   
  19. struct string_compare  
  20. {  
  21.     bool operator () (string const& str1, string const& str2)  
  22.     {  
  23.         return str1 == str2;  
  24.     }  
  25. };  
  26.   
  27. int main()  
  28. {  
  29.     clock_t beg = clock();  
  30.   
  31.     hash_map<string, int, string_hash, string_compare> hastable;  
  32.   
  33.     ifstream fi ("test.txt");  
  34.     ofstream fo ("test_dst.txt");  
  35.       
  36.     string line;  
  37.     size_t count = 0;  
  38.       
  39.     for (getline(fi, line); !fi.eof(); getline(fi, line))  
  40.     {  
  41.         if (hastable.find(line) == hastable.end())  
  42.         {  
  43.             hastable[line] = 1;   
  44.             fo << line << endl;  
  45.             ++count;  
  46.         }  
  47.     }  
  48.       
  49.     fo.close();  
  50.     fi.close();  
  51.       
  52.     clock_t end = clock();  
  53.     cout << "count = " << count << ", time = " << (end-beg) << "ms" << endl;  
  54.   
  55.     return 0;  
  56. }  

寫的代碼和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:
  1. int main()  
  2. {  
  3.     clock_t beg = clock();  
  4.    
  5.     //最佳化一:給hastable預設一個足夠大的數目  
  6.     hash_map<string, int, string_hash, string_compare> hastable(40050);  
  7.    
  8.     ifstream fi ("test.txt");  
  9.     istringstream ssi;  
  10.       
  11.     fi.seekg(0, ifstream::end);  
  12.     size_t fileSize = fi.tellg();  
  13.     fi.seekg(0);  
  14.           
  15.     //最佳化二:一次讀入檔案的全部內容到記憶體  
  16.     char* buf = new char[fileSize];  
  17.     fi.read(buf, fileSize);   
  18.       
  19.     //最佳化三:從存有資料的那段記憶體,直接構造出"記憶體流", 避免複製  
  20.     ssi.rdbuf()->pubsetbuf(buf, fi.gcount());  
  21.     fi.close();   
  22.       
  23.     string line;  
  24.     size_t count = 0;  
  25.   
  26.     ostringstream sso;  
  27.       
  28.     //最佳化四:從記憶體流讀每一行,而不是從檔案流  
  29.     for(getline(ssi, line); !ssi.eof(); getline(ssi, line))  
  30.     {  
  31.         if (hastable.find(line) == hastable.end())  
  32.         {  
  33.             hastable[line] = 1;   
  34.             //最佳化五:先輸出到記憶體流,而不是到檔案流  
  35.             sso << line << endl;  
  36.             ++count;  
  37.         }  
  38.     }  
  39.     delete [] buf;  
  40.       
  41.     ofstream fo ("test_dst.txt");  
  42.     fo << sso.str() << endl;  
  43.     fo.close();  
  44.       
  45.     clock_t end = clock();  
  46.     cout << "count = " << count << ", time = " << (end - beg) << "ms" << endl;  
  47.   
  48.     return 0;  
  49. }  

     取檔案大小,沒有考慮超過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;

使用

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.