入職一周,還算比較清閑。沒有一些明確時間點的事情,所以目前的大部分時候,探索成分居多,絞盡腦汁做的某些架構設計,不談結果,就過程而言,收獲頗豐。
總的來說,蠻喜歡目前的狀態,思考>編碼。
今天下午,旁邊的同事在做一個關于庫上的檢查,簡單說就是判斷數據庫的某些字段是否唯一。
最開始是一條sql語句,大概是這樣的:
select * from aaa group by aaa.bbb, aaa.ccc having count(*) > 1;
全表大概1000w的數據,第一次執行,用了16分鐘。
我們的mysql是架在一臺12核,128Gb內存的機器上,應該說資源是足夠的,但是這樣的執行時間,相比于oracle的2分鐘左右的處理速度,還是慢了太多。
單純select *大概在1分鐘左右,所以大部分的開銷在group by上,從這點上看,mysql的一些計算能力確實欠缺太多。
后來考慮優化,將select *的結果全部裝入內存,然后進行查找。
思路上,沒有任何問題,但他的實施是這樣的:
判斷重復時,使用了2個循環,如下:
vector<string> data;
for (int i = 0; i < data.size(); i++)
{
for (int j = i + 1; j < data.size(); j++)
{
if (data[i] == data[j])
{
//do some
}
}
}
這個代碼執行了大概30分鐘,仍然沒有結果。
正好當時沒事,就幫他做了一些優化。很顯然,這樣的一個唯一性檢查使用窮舉是有問題的,復雜度在O(N^2),對于1000w這樣的數量級,O(N^2)基本上是不可實現的。所以第一步的優化,就是將這個算法的復雜度降下來。
增加一個排序操作,這樣,程序的處理流程將變為:
1 排序
2 兩兩比較,確定非唯一值
總體的復雜度為O(N logN)
在排序中,使用了stl的sort函數進行排序,程序整體執行一遍的時間為7分鐘。刨掉出庫select的1分鐘,排序的時間在6分鐘左右。這么說來,還是有些慢。這個時候,更好的優化方式是將算法更改為hash,這樣時間復雜度將降為O(N),那么將會有質的提升,但是考慮到stl的map是紅黑樹實現,復雜度為O(N logN),并不是很好的選擇,而自己重新寫一個hash函數好像又比較麻煩。所以權衡了一下,還是繼續從排序入手。
最開始,想要將排序操作轉移到mysql上執行,也就是在出庫的同時進行排序,需要把出庫語句改為:
select * from aaa order by aaa.bbb, aaa.ccc;
這樣之后,內存中只需要一次遍歷就好,時間開銷基本可以忽略,這條sql的執行時間為4分鐘,雖然相比于前面有了一些提升,但還是不夠理想,還需要進一步的優化。
在上文的程序中,我們發現全部的數據存儲在一個vector里,每一條數據為一個string,因此核心的開銷為string的operator < 比較操作。string是一個相對來說比較龐大的類,會造成許多的額外開銷。
我們只需要比較2個字段的唯一性,完全可以將這兩個字段拼接起來,存在一個char*中,之后的比較基于memcmp。
當然,可能有這樣的問題,第一條記錄的第一個字段為“123”,第二個字段為“456”;而第二條記錄的第一個字段為“12”,第二個字段為“3456”,這樣本來不等的兩個字段,拼接后相同。解決方式也很簡單,在“123”的結束位置加入一個特殊字符,再拼接,這樣就可以了。
在更改為char*之后,數據的排序時間從原來的3分鐘降到了40秒。
目前來看,總體的執行時間為1分鐘40秒,其中出庫1分鐘,排序40秒。系統的整體瓶頸轉移到了出庫這里,這里的優化方式就相當明顯了,我們只是用到了2個字段,完全沒有必要select *,只需要將select語句改為:
select aaa.bbb, aaa.ccc from aaa;
這樣更改后,出庫的時間為13秒,整體的執行時間為50秒。
這個時間,已經完全符合要求了,但是為了追求更高的速度,繼續將計算并行化,也就是說,充分利用cpu的多核計算能力,將任務盡可能的拆分為多個線程來完成。對于本例來說,我們并不需要整體有序的數據,因此,考慮hadoop的分桶思想,我們將取出的char*每位相加,得到的結果對10取余,將結果分散到10個線程去做,最后的執行結果為4秒鐘。
經過上面的一步步優化,總共的執行時間為17秒。
(原文時間2014-1-15)