一次優化Lua算法的經歷

問題是這樣的:有一個匹配文件,內容大概是這樣的:


3145pcl.gpd
3155pcl.gpd
3165pcl.gpd
31x5hc01.cnt
31x5hc01.hlp
31x5hs01.cnt
31x5hs01.hlp
31x5lc04.dll
...


還有一些需要處理的文件,要求按行處理,如果某行帶有匹配文件中任意行的內容(不區分大小寫),那麼就刪掉該行。
問題看似簡單,但是在文件行數十分巨大的情況下,傳統的做法性能相當差勁!
傳統的設計無非是2重循環:
第一種,外層循環待處理文件,內層循環匹配文件:


循環讀待處理文件的每一行 {
    循環比較匹配文件的每一行 {
        如果匹配,則退出循環
    }
    如果不匹配,則寫到輸出文件中
}


第二種,外層循環匹配文件,內層循環待處理文件:


循環匹配文件的每一行 {
    比較待處理文件未打上標記的行 {
        如果匹配,則給這一行打上標記
    }
}
把未打上標記的行寫到輸出文件中

 

(其實還有一種:把待處理文件不分行,看作整體用正則表達式替換,不過性能還要低下!)
上述兩種算法雖然作了一些小的優化,但是基本上覆雜度是和匹配文件行數*待處理文件行數成正比的,在此例中匹配文件有3782行,需要處理的文件加起來總共40187行,乘積達到了上億的數量級。
寫了好幾個版本,在正則表達式等細微的地方做了很多嘗試,但是時間始終在50~80秒之間徘徊,同樣的工作,Unix下面的grep工具竟然能夠瞬間完成,性能差距有些不可思議。看起來已經沒有優化的餘地了(我曾經想過類似於MapReduce的多線程操作,可惜Lua不支持...)。
後來在一次偶然的測試中,發現原來表的循環操作是罪魁禍首:我註釋了最內層的匹配過程,時間僅下降幾十秒,但是如果註釋了內層循環,那麼基本上是一瞬間就完成了。
後來在網上看了相關資料(例如:http://trac.caspring.org/wiki/LuaPerformance),在Lua中,要儘量避免使用pairs和ipairs的循環操作,非常費時的,另外《Programming in Lua》一書的11.5節講到了如何構建Set的技巧,非常有用,OK,就此重新設計算法:
首先構造匹配文件對應的Set,字符串已經全部轉換爲小寫:

 

-- 匹配字符串序列
matchstrs = {};
-- 匹配字符串最大長度
maxmatchstrlen = 0;
for line in io.lines("matchstr.txt") do
    matchstrs[line:lower()] = true;
    local len = line:len();
    if len > maxmatchstrlen then
        maxmatchstrlen = len;
    end;
end;

 

使用此Set非常簡單:“matchstrs[字符串]”直接返回其中是否包含該字符串,使用此Set構造匹配函數:

 

-- 函數:判斷字符串是否包含匹配字符串序列之一,不區分大小寫,返回true或false
function match(str)
    -- 字符串轉換爲小寫
    local lowerstr = str:lower();
    -- 獲得字符串長度
    local lowerstrlen = lowerstr:len();
    -- 子串的頭位置
    local head;
    -- 子串的尾位置
    local tail;
    for head = 1, lowerstrlen do
        for tail = head, math.min(head + maxmatchstrlen, lowerstrlen) do
            if matchstrs[lowerstr:sub(head, tail)] then
                return true;
            end;
        end;
    end;
    return false;
end;


其中雖然也用到了二重循環,但是其性能和表循環根本不是一個數量級的。
算法經過優化後,實測時間4秒!4秒,什麼概念?Oh Yeah的概念,哈哈!

 

下面是完整代碼:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章