還是上次那個苦逼的web搜尋作業,本來做完了就算了吧。手賤的我有把程式塞給vtune跑了跑,發現了幾個熱點程式之後,我就不淡定了。好像總是有點淡淡的憂怨在心頭無法釋懷。於是就走上了最佳化這條不歸路。其實這條不歸路是很容易走完的。尤其是像我這樣的菜鳥,把所有最佳化的招的用完了,這條路就走到了盡頭。我就不曬我的文采了,直接講重點吧。
背景知識:我的主要工作是構建一個文檔向量。關於這個文檔向量的定義可看我的上一篇部落格。我用的最多的是統計一個單詞出現了多少詞,還有就是一個單詞出現在幾個文檔中。我採用了std::map<string,int>這種資料結構,可以方便我找到一個單詞出現了多少詞。我的最佳化就是從坑爹的std::map使用開始的。。。
我使用的vtune版本是VTune Amplifier XE 2011.編程環境是fedora 15.選擇的類型是hot function。點擊start按鈕,VTune就開始分析熱點程式了。第一次啟動並執行時候需要統計的文檔比較少。已耗用時間是0.24s,得到的第一個熱點函數是AccFreqIdf。這個函數的功能是統計一個單詞出現了幾次,和一個單詞出現在幾個文檔中。 雙擊函數名按鈕可以開啟相應的源檔案。發現一下代碼的第一行是個瓶頸:
1 if( !m_IDF[keyword].showup ) //hot function detected by vtune
2 {
3 m_IDF[keyword].num++;
4 m_IDF[keyword].showup = true;
5 }
對於一個關鍵詞出現在幾個文檔中這個問題我一開始想了這麼一個法子:定義一個結構體包括int num和bool showup;初始狀況下,showup為false。在一個文檔中一個關鍵字只要出現了,他的showup就是true。從此以後無論出現幾個該關鍵字num都不會增加了。if語句可以保證在一個文檔中某關鍵字只要出現了,計數就增加1.而不管他出現了幾次。但是這個判斷語句使用的太頻繁了,成為了一個hotfunction。於是就有了下面的代碼:
1 map<string,int> tmpIDF;
2 tmpIDF.clear();
3
4 while(!file.eof())
5 {
6 //read in every words
7 file >> keyword;
8 //tmpIDF.clear();
9 tmpIDF[keyword] = 1;//set keyword to 1
10 //----------------------------
11 //acc idf
12 //if( !m_IDF[keyword].showup ) //hot function detected by vtune
13 //{
14 // m_IDF[keyword].num++;
15 //m_IDF[keyword].showup = true;
16 //}
17 timePerWord[keyword]++;
18
19 }//end while
20 //insert showup keywords to m_IDF map
21 map<string,int>::iterator iterTmpIdf = tmpIDF.begin();
22 for( ; iterTmpIdf != tmpIDF.end(); iterTmpIdf++)
23 {
24 //cout << iterTmpIdf->first << endl;
25 m_IDF[iterTmpIdf->first].num++;
26 }
27 }
在每一個檔案內我定義了一個std::map<string,int> tmpIDF。如第9行所示,在這個文檔內出現的所有關鍵字都插入到tmpIDF中。tmpIDF只儲存該檔案出現了哪些關鍵字而不去管每個關鍵字出現了幾次。統計完當前檔案之後,將tmpIDF合并到m_IDF這個大map中。如第21至26行所示。我使用m_IDF這個map來儲存所有文檔的每個關鍵字出現在幾個文檔中。這樣可以避免if語句的使用。再送入vtune分析,發現效率得到了明顯的改觀。已耗用時間從0.24s降低到了0.12s。
你可能會說這個已耗用時間太短了,如果文檔數量增加會怎麼樣呢?當我增加文檔的數量之後發現程式已耗用時間是2.4s 有了一個新的瓶頸。熱點函數變成了CalcTFIDF這個函數中的如下代碼的第4行:
1 for(map<string,NumIDF>::iterator idfIt = m_IDF.begin(); idfIt!=m_IDF.end(); ++idfIt)
2 {
3 //for every keywords
4 int freqij = elem[idfIt->first];
5 int maxj = iter->second;
6 int N = m_MaxFreq.size();
7 float ni = (float)idfIt->second.num;
8 float tmp = (freqij/(float)maxj)*log((float)N/ni);
9 rkdv.push_back(tmp);
10 }
這段代碼的功能是構建文檔向量。舉個例子來說明文檔向量吧:
現在一個三個文檔a.txt b.txt 和c.txt。a.txt中的關鍵字有happy。b.txt中的關鍵字有merry. c.txt中的關鍵字有new year。所有關鍵字各出現一次。所有可以構建如下的向量:
happy merry new year
a.txt 1 0 0 0
b.txt 0 1 0 0
c.txt 0 0 1 1
於是得到的文檔向量分別是:a.txt(1,0,0,0) b.txt(0,1,0,0) c.txt(0,0,1,1)。當然這隻是個例子,真正的計算還要複雜一些如上面代碼第8行。
在老方法中,我把每個文檔出現的關鍵字都存在elem這個變數中。m_IDF中儲存的是所有文檔所有的關鍵字。我把所有文檔中所有的關鍵字都用elem[idfIt->first]來查詢一下。比如說。a.txt的map是這樣存的:(happy,1)(happy 出現一次)。但是執行完elem["merry"];elem["new"];elem["year"]之後,雖然可以得到正確的結果,但是operator []是有插入功能的,於是現在a.txt存的是這樣的一個map:(happy,1)(merry ,0)(new ,0)(year,0).有很多不必要的操作 。所以這個函數非常耗時間。於是我採用了如下的改進方法:
1 map<string,NumIDF>::iterator iter = m_IDF.begin();
2 int index = 0;
3 for(; iter != m_IDF.end(); iter++)
4 {
5 iter->second.showup = index++;
6 }
7 .....
8 ......
9
10 for( ; elemIter != elem.end(); elemIter++)
11 {
12 //get index in vector
13 const NumIDF& nidf = m_IDF[elemIter->first];
14 int indVec = nidf.showup;
15 //get the pointer to write to
16 advance(dvIter,indVec - lastInd);
17 lastInd = indVec;
18 int freqij = elemIter->second;
19 int maxj = iter->second;
20 int N = m_MaxFreq.size();
21 float ni = (float)nidf.num;
22 float tmp = (freqij/(float)maxj)*log((float)N/ni);
23 *dvIter = tmp;
24 }
文檔向量我是用vector<float>來儲存的。但是這個向量有很多值是0.因為在該文檔中不會出現所有的關鍵字。首先我遍曆了一次m_IDF這個map把每個關鍵字出現在幾個文檔中和該關鍵字在m_IDF是第幾個存在了一個教NumIDF的結構體中。對於每個文檔我只遍曆該文檔中出現的關鍵字。尋找該關鍵字在m_IDF中是第幾個。於是就把vector<float>向量的第幾個元素設定成非0的值。至於vector<float>在其他位置上的值設定為0.這樣之後程式的已耗用時間從2.5s減少到了0.5s。高興了我一個上午,而且我打算交作業了,不最佳化了。。。