關於python的一次性能調優過程


問題

這兩天在公司幫老大寫一個程序功能,要求抓取從elasticsearch和kibana服務器上返回的數據,統計所有hits的數據字段ret_code為0的hit,並計算其占有率等一些功能。
功能倒是寫完交差和合並主分支了,但是后來試運行卻發現統計完所有response的數據並且發送報警郵件的整個過程居然要兩個小時之久!我想雖然python的性能是比不上java但是也沒有這么差勁吧。后來調試發現,原來的程序運行只有六七分鍾,我開始懷疑我寫的代碼巨爛。后來經過調試比對,發現並不是我加進去的代碼有問題,而是之前同事寫的代碼欠缺一部分性能和擴展性的考慮。以下是我的分析過程。

分析過程

打開pycharm run菜單下的profile選項,利用profile工具對程序運行的性能指標進行計算和分析。

得到的函數調用時間占比圖的大體結果如下,我們可以得到每個函數(方框)運行的時間及其在它所調用的其他函數。紅色即代表該函數占用整體運行時間的比率太高,說明該函數是整個程序運行的性能瓶頸。反而顏色越綠占比就越小。

我們放大其中紅色的部分,我們可以發現所調用的我們自己寫的函數__normalize_url函數占用了大部分的運行時間。從函數名知道這是一個對url正則化的函數,其中用到了re模塊的match和_compile函數,而最終程序所浪費的時間的關鍵就在於最后的re中的compile函數。

def __normalize_url(url):
   norm_url = re.sub('\?.*$', '', url) if '?' in url else url
   for mapping in url_normalization_mappings:
      if re.match(mapping[0], norm_url):
      return mapping[1]
   return norm_url

這段代碼調用的re.compile()函數看似沒有問題,但其中存在一些不足。我們點開re模塊的match函數的實現。是對pattern正則字符串進行編譯成正則化對象,再對目標string進行匹配。

def match(pattern, string, flags=0):
    """Try to apply the pattern at the start of the string, returning
    a match object, or None if no match was found."""
    return _compile(pattern, flags).match(string)

再來看_compile函數

_cache = {}

_pattern_type = type(sre_compile.compile("", 0))

_MAXCACHE = 512
def _compile(pattern, flags):
    # internal: compile pattern
    try:
        p, loc = _cache[type(pattern), pattern, flags]
        if loc is None or loc == _locale.setlocale(_locale.LC_CTYPE):
            return p
    except KeyError:
        pass
    if isinstance(pattern, _pattern_type):
        if flags:
            raise ValueError(
                "cannot process flags argument with a compiled pattern")
        return pattern
    if not sre_compile.isstring(pattern):
        raise TypeError("first argument must be string or compiled pattern")
    p = sre_compile.compile(pattern, flags)
    if not (flags & DEBUG):
        if len(_cache) >= _MAXCACHE:
            _cache.clear()
        if p.flags & LOCALE:
            if not _locale:
                return p
            loc = _locale.setlocale(_locale.LC_CTYPE)
        else:
            loc = None
        _cache[type(pattern), pattern, flags] = p, loc
    return p

我們發現,_compile函數會設置一個緩存,保存編譯過的正則化對象,這樣以后我們再想對相同的正則規則字符串進行編譯時就可以直接取該對象而不用花時間來重復編譯它。而這個緩存對象的個數限制就為_MAXCACHE 即原代碼中設置的512。
而原來我們寫的程序要匹配的url有496個,而經過后來接口的增加,url增加到了516個,正好突破了緩存的個數限制!然后緩存居然清空了!(如下)

       if len(_cache) >= _MAXCACHE:
            _cache.clear()

也就是說,當我們匹配每一個url時,如果緩存溢出了都要重新編譯所有的正則化對象,這無形中浪費了大量的時間。更何況我們要匹配接近十萬條的url,這也難怪要要花費兩個多小時的時間來運行了,幾乎都浪費在了編譯正則化對象上。

改進

原本我們可以手動更改_MAXCACHE的大小,但是要跨平台運行,所以我們可以手動自己造一個簡單的緩存。

# 緩存器
_url_patterns_max_cache=512 if len(url_patterns)<=512 else (len(url_patterns)/256+1)*256
_url_patterns_cache={}
for url_pattern in url_patterns:	
if len(_url_patterns_cache)>_url_patterns_max_cache):
   _url_patterns_cache.clear()
url_patterns_cache.update({url_pattern:re.compile(url_pattern)})

# 直接從緩存中取值進行匹配
for url_pattern_key in _url_patterns_cache:
	if _url_patterns_cache.get(url_pattern_key).match(url):
        return url_pattern_key

最后我們只用緩存中的編譯好的對象直接進行匹配即可,整個過程每一個規則只用編譯一次,極大的節省了時間。~以下是經過優化后的性能表現。總花時382233 ms約為6.37 minutes ,完美解決!

感想

說一點感想,這是我實習第一次要做的任務,雖然沒啥難度,也可讓我抓耳撓腮了一陣子了。本來都做好了,偏偏又出了這個性能問題,花了我一兩天時間排查和改進,其中我還學習了profile工具。不得不說,我還是太年輕了。人生還需要不斷地學習和鞏固知識。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM