1、使用生成器和列表解析
一個普遍被忽略的內(nèi)存優(yōu)化是生成器的使用。生成器讓我們創(chuàng)建一個函數(shù)一次只返回一條記錄,而不是一次返回所有的記錄,如果你正在使用python2.x,這就是你為啥使用xrange替代range或者使用ifilter替代filter的原因。一個很好地例子就是創(chuàng)建一個很大的列表并將它們拼合在一起。
- import timeit
- import random
-
- def generate(num):
- while num:
- yield random.randrange(10)
- num -= 1
-
- def create_list(num):
- numbers = []
- while num:
- numbers.append(random.randrange(10))
- num -= 1
- return numbers
-
- print(timeit.timeit("sum(generate(999))", setup="from __main__ import generate", number=1000))
- print(timeit.timeit("sum(create_list(999))", setup="from __main__ import create_list", number=1000))
輸出:
1.00842191191
0.933518458666
列表解析要比在循環(huán)中重新構(gòu)建一個新的 list 更為高效,因此我們可以利用這一特性來提高運行的效率。
- from time import time
- t = time()
- list = ['a','b','is','python','jason','hello','hill','with','phone','test',
- 'dfdf','apple','pddf','ind','basic','none','baecr','var','bana','dd','wrd']
- total=[]
- for i in range(1000000):
- for w in list:
- total.append(w)
- print "total run time:"
- print time()-t
使用列表解析:
- for i in range(1000000):
- a = [w for w in list]
上述代碼直接運行大概需要 17s,而改為使用列表解析后 ,運行時間縮短為 9.29s。將近提高了一半。生成器表達式則是在 2.4 中引入的新內(nèi)容,語法和列表解析類似,但是在大數(shù)據(jù)量處理時,生成器表達式的優(yōu)勢較為明顯,它并不創(chuàng)建一個列表,只是返回一個生成器,因此效率較高。在上述例子上中代碼 a = [w for w in list] 修改為 a = (w for w in list),運行時間進一步減少,縮短約為 2.98s。
2、Ctypes的介紹
對于關(guān)鍵性的性能代碼python本身也提供給我們一個API來調(diào)用C方法,主要通過 ctypes來實現(xiàn),你可以不寫任何C代碼來利用ctypes。默認情況下python提供了預編譯的標準c庫,我們再回到生成器的例子,看看使用ctypes實現(xiàn)花費多少時間。
- import timeit
- from ctypes import cdll
-
- def generate_c(num):
- #Load standard C library
- #libc = cdll.LoadLibrary("libc.so.6") #Linux
- libc = cdll.msvcrt #Windows
- while num:
- yield libc.rand() % 10
- num -= 1
-
- print(timeit.timeit("sum(generate_c(999))", setup="from __main__ import generate_c", number=1000))
輸出:
0.404974439902
僅僅換成了c的隨機函數(shù),運行時間減了大半!現(xiàn)在如果我告訴你我們還能做得更好,你信嗎?
3、Cython的介紹
Cython 是python的一個超集,允許我們調(diào)用C函數(shù)以及聲明變量來提高性能。嘗試使用之前我們需要先安裝Cython.
Cython 本質(zhì)上是另一個不再開發(fā)的類似類庫Pyrex的分支,它將我們的類Python代碼編譯成C庫,我們可以在一個python文件中調(diào)用。對于你的python文件使用.pyx后綴替代.py后綴,讓我們看一下使用Cython如何來運行我們的生成器代碼。
- #cython_generator.pyx
- import random
-
- def generate(num):
- while num:
- yield random.randrange(10)
- num -= 1
我們需要創(chuàng)建個setup.py以便我們能獲取到Cython來編譯我們的函數(shù)。
- from distutils.core import setup
- from distutils.extension import Extension
- from Cython.Distutils import build_ext
-
- setup(
- cmdclass = {'build_ext': build_ext},
- ext_modules = [Extension("generator", ["cython_generator.pyx"])]
- )
編譯使用:
python setup.py build_ext - - inplace
你應該可以看到兩個文件cython_generator.c 文件和generator.so文件,我們使用下面方法測試我們的程序:
- import timeit
- print(timeit.timeit("sum(generator.generate(999))", setup="import generator", number=1000))
- >>> 0.835658073425
還不賴,讓我們看看是否還有可以改進的地方。我們可以先聲明“num”為整形,接著我們可以導入標準的C庫來負責我們的隨機函數(shù)。
- #cython_generator.pyx
- cdef extern from "stdlib.h":
- int c_libc_rand "rand"()
-
- def generate(int num):
- while num:
- yield c_libc_rand() % 10
- num -= 1
如果我們再次編譯運行我們會看到這一串驚人的數(shù)字。
僅僅的幾個改變帶來了不賴的結(jié)果。然而,有時這個改變很乏味,因此讓我們來看看如何使用規(guī)則的python來實現(xiàn)吧。
4、PyPy的介紹
PyPy 是一個Python2.7.3的即時編譯器(JIT),通俗地說這意味著讓你的代碼運行的更快。Quora在生產(chǎn)環(huán)境中使用了PyPy。PyPy在它們的下載頁面有一些安裝說明,但是如果你使用的Ubuntu系統(tǒng),你可以通過apt-get來安裝。它的運行方式是立即可用的,因此沒有瘋狂的bash或者運行腳本,只需下載然后運行即可。讓我們看看我們原始的生成器代碼在PyPy下的性能如何。
- import timeit
- import random
-
- def generate(num):
- while num:
- yield random.randrange(10)
- num -= 1
-
- def create_list(num):
- numbers = []
- while num:
- numbers.append(random.randrange(10))
- num -= 1
- return numbers
-
- print(timeit.timeit("sum(generate(999))", setup="from __main__ import generate", number=1000))
- >>> 0.115154981613 #PyPy 1.9
- >>> 0.118431091309 #PyPy 2.0b1
- print(timeit.timeit("sum(create_list(999))", setup="from __main__ import create_list", number=1000))
- >>> 0.140175104141 #PyPy 1.9
- >>> 0.140514850616 #PyPy 2.0b1
5、進一步測試
為什么還要進一步研究?PyPy是冠軍!并不全對。雖然大多數(shù)程序可以運行在PyPy上,但是還是有一些庫沒有被完全支持。而且,為你的項目寫C的擴展相比換一個編譯器更加容易。讓我們更加深入一些,看看ctypes如何讓我們使用C來寫庫。我們來測試一下歸并排序和計算斐波那契數(shù)列的速度。下面是我們要用到的C代碼(functions.c):
- /* functions.c */
- #include "stdio.h"
- #include "stdlib.h"
- #include "string.h"
-
- /* http:///wiki/Sorting_algorithms/Merge_sort#C */
- inline
- void merge(int *left, int l_len, int *right, int r_len, int *out)
- {
- int i, j, k;
- for (i = j = k = 0; i < l_len && j < r_len; )
- out[k++] = left[i] < right[j] ? left[i++] : right[j++];
-
- while (i < l_len) out[k++] = left[i++];
- while (j < r_len) out[k++] = right[j++];
- }
-
- /* inner recursion of merge sort */
- void recur(int *buf, int *tmp, int len)
- {
- int l = len / 2;
- if (len <= 1) return;
-
- /* note that buf and tmp are swapped */
- recur(tmp, buf, l);
- recur(tmp + l, buf + l, len - l);
-
- merge(tmp, l, tmp + l, len - l, buf);
- }
-
- /* preparation work before recursion */
- void merge_sort(int *buf, int len)
- {
- /* call alloc, copy and free only once */
- int *tmp = malloc(sizeof(int) * len);
- memcpy(tmp, buf, sizeof(int) * len);
-
- recur(buf, tmp, len);
-
- free(tmp);
- }
-
- int fibRec(int n){
- if(n < 2)
- return n;
- else
- return fibRec(n-1) + fibRec(n-2);
- }
在Linux平臺,我們可以用下面的方法把它編譯成一個共享庫:
gcc - Wall - fPIC - c
functions.c
gcc - shared - o libfunctions.so functions.o
使用ctypes, 通過加載”libfunctions.so”這個共享庫,就像我們前邊對標準C庫所作的那樣,就可以使用這個庫了。這里我們將要比較Python實現(xiàn)和C實現(xiàn)?,F(xiàn)在我們開始計算斐波那契數(shù)列:
- #functions.py
- from ctypes import *
- import time
-
- libfunctions = cdll.LoadLibrary("./libfunctions.so")
-
- def fibRec(n):
- if n < 2:
- return n
- else:
- return fibRec(n-1) + fibRec(n-2)
-
- start = time.time()
- fibRec(32)
- finish = time.time()
- print("Python: " + str(finish - start))
-
- #C Fibonacci
- start = time.time()
- x = libfunctions.fibRec(32)
- finish = time.time()
- print("C: " + str(finish - start))
- Python: 1.18783187866 #Python 2.7
- Python: 1.272292137145996 #Python 3.2
- Python: 0.563600063324 #PyPy 1.9
- Python: 0.567229032516 #PyPy 2.0b1
- C: 0.043830871582 #Python 2.7 + ctypes
- C: 0.04574108123779297 #Python 3.2 + ctypes
- C: 0.0481240749359 #PyPy 1.9 + ctypes
- C: 0.046403169632 #PyPy 2.0b1 + ctypes
正如我們預料的那樣,C比Python和PyPy更快。我們也可以用同樣的方式比較歸并排序。
我們還沒有深挖Cypes庫,所以這些例子并沒有反映python強大的一面,Cypes庫只有少量的標準類型限制,比如int型,char數(shù)組,float型,字節(jié)(bytes)等等。默認情況下,沒有整形數(shù)組,然而通過與c_int相乘(ctype為int類型)我們可以間接獲得這樣的數(shù)組。這也是代碼第7行所要呈現(xiàn)的。我們創(chuàng)建了一個c_int數(shù)組,有關(guān)我們數(shù)字的數(shù)組并分解打包到c_int數(shù)組中
主要的是c語言不能這樣做,而且你也不想。我們用指針來修改函數(shù)體。為了通過我們的c_numbers的數(shù)列,我們必須通過引用傳遞merge_sort功能。運行merge_sort后,我們利用c_numbers數(shù)組進行排序,我已經(jīng)把下面的代碼加到我的functions.py文件中了。
- #Python Merge Sort
- from random import shuffle, sample
-
- #Generate 9999 random numbers between 0 and 100000
- numbers = sample(range(100000), 9999)
- shuffle(numbers)
- c_numbers = (c_int * len(numbers))(*numbers)
-
- from heapq import merge
-
- def merge_sort(m):
- if len(m) <= 1:
- return m
-
- middle = len(m) // 2
- left = m[:middle]
- right = m[middle:]
-
- left = merge_sort(left)
- right = merge_sort(right)
- return list(merge(left, right))
-
- start = time.time()
- numbers = merge_sort(numbers)
- finish = time.time()
- print("Python: " + str(finish - start))
-
- #C Merge Sort
- start = time.time()
- libfunctions.merge_sort(byref(c_numbers), len(numbers))
- finish = time.time()
- print("C: " + str(finish - start))
- Python: 0.190635919571 #Python 2.7
- Python: 0.11785483360290527 #Python 3.2
- Python: 0.266992092133 #PyPy 1.9
- Python: 0.265724897385 #PyPy 2.0b1
- C: 0.00201296806335 #Python 2.7 + ctypes
- C: 0.0019741058349609375 #Python 3.2 + ctypes
- C: 0.0029308795929 #PyPy 1.9 + ctypes
- C: 0.00287103652954 #PyPy 2.0b1 + ctypes
這兒通過表格和圖標來比較不同的結(jié)果。
|
Merge Sort |
Fibonacci |
Python 2.7 |
0.191 |
1.187 |
Python 2.7 + ctypes |
0.002 |
0.044 |
Python 3.2 |
0.118 |
1.272 |
Python 3.2 + ctypes |
0.002 |
0.046 |
PyPy 1.9 |
0.267 |
0.564 |
PyPy 1.9 + ctypes |
0.003 |
0.048 |
PyPy 2.0b1 |
0.266 |
0.567 |
PyPy 2.0b1 + ctypes |
0.003 |
0.046 |
代碼優(yōu)化能夠讓程序運行更快,它是在不改變程序運行結(jié)果的情況下使得程序的運行效率更高,根據(jù) 80/20 原則,實現(xiàn)程序的重構(gòu)、優(yōu)化、擴展以及文檔相關(guān)的事情通常需要消耗 80% 的工作量。優(yōu)化通常包含兩方面的內(nèi)容:減小代碼的體積,提高代碼的運行效率。
6、改進算法,選擇合適的數(shù)據(jù)結(jié)構(gòu)
一個良好的算法能夠?qū)π阅芷鸬疥P(guān)鍵作用,因此性能改進的首要點是對算法的改進。在算法的時間復雜度排序上依次是:
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
因此如果能夠在時間復雜度上對算法進行一定的改進,對性能的提高不言而喻。但對具體算法的改進不屬于本文討論的范圍,讀者可以自行參考這方面資料。下面的內(nèi)容將集中討論數(shù)據(jù)結(jié)構(gòu)的選擇。
● 字典 (dictionary) 與列表 (list)
Python 字典中使用了hash table,因此查找操作的復雜度為 O(1),而 list 實際是個數(shù)組,在 list 中,查找需要遍歷整個 list,其復雜度為 O(n),因此對成員的查找訪問等操作字典要比 list 更快。
清單 1. 代碼 dict.py
- from time import time
- t = time()
- list = ['a','b','is','python','jason','hello','hill','with','phone','test',
- 'dfdf','apple','pddf','ind','basic','none','baecr','var','bana','dd','wrd']
- #list = dict.fromkeys(list,True)
- print list
- filter = []
- for i in range (1000000):
- for find in ['is','hat','new','list','old','.']:
- if find not in list:
- filter.append(find)
- print "total run time:"
- print time()-t
上述代碼運行大概需要 16.09seconds。如果去掉行 #list = dict.fromkeys(list,True) 的注釋,將 list 轉(zhuǎn)換為字典之后再運行,時間大約為 8.375 seconds,效率大概提高了一半。因此在需要多數(shù)據(jù)成員進行頻繁的查找或者訪問的時候,使用 dict 而不是 list 是一個較好的選擇。
● 集合 (set) 與列表 (list)
set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的問題可以轉(zhuǎn)換為 set 來操作。
清單 2. 求 list 的交集:
- from time import time
- t = time()
- lista=[1,2,3,4,5,6,7,8,9,13,34,53,42,44]
- listb=[2,4,6,9,23]
- intersection=[]
- for i in range (1000000):
- for a in lista:
- for b in listb:
- if a == b:
- intersection.append(a)
-
- print "total run time:"
- print time()-t
上述程序的運行時間大概為:
total run time:
38.4070000648
清單 3. 使用 set 求交集
- from time import time
- t = time()
- lista=[1,2,3,4,5,6,7,8,9,13,34,53,42,44]
- listb=[2,4,6,9,23]
- intersection=[]
- for i in range (1000000):
- list(set(lista)&set(listb))
- print "total run time:"
- print time()-t
改為 set 后程序的運行時間縮減為 8.75,提高了 4 倍多,運行時間大大縮短。讀者可以自行使用表 1 其他的操作進行測試。
表 1. set 常見用法
語法 |
操作 |
說明 |
set(list1) | set(list2) |
union |
包含 list1 和 list2 所有數(shù)據(jù)的新集合 |
set(list1) & set(list2) |
intersection |
包含 list1 和 list2 中共同元素的新集合 |
set(list1) - set(list2) |
difference |
在 list1 中出現(xiàn)但不在 list2 中出現(xiàn)的元素的集合 |
7、對循環(huán)的優(yōu)化
對循環(huán)的優(yōu)化所遵循的原則是盡量減少循環(huán)過程中的計算量,有多重循環(huán)的盡量將內(nèi)層的計算提到上一層。 下面通過實例來對比循環(huán)優(yōu)化后所帶來的性能的提高。程序清單 4 中,如果不進行循環(huán)優(yōu)化,其大概的運行時間約為 132.375。
清單 4. 為進行循環(huán)優(yōu)化前
- from time import time
- t = time()
- lista = [1,2,3,4,5,6,7,8,9,10]
- listb =[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.01]
- for i in range (1000000):
- for a in range(len(lista)):
- for b in range(len(listb)):
- x=lista[a]+listb[b]
- print "total run time:"
- print time()-t
現(xiàn)在進行如下優(yōu)化,將長度計算提到循環(huán)外,range 用 xrange 代替,同時將第三層的計算 lista[a] 提到循環(huán)的第二層。
清單 5. 循環(huán)優(yōu)化后
- from time import time
- t = time()
- lista = [1,2,3,4,5,6,7,8,9,10]
- listb =[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.01]
- len1=len(lista)
- len2=len(listb)
- for i in xrange (1000000):
- for a in xrange(len1):
- temp=lista[a]
- for b in xrange(len2):
- x=temp+listb[b]
- print "total run time:"
- print time()-t
上述優(yōu)化后的程序其運行時間縮短為 102.171999931。在清單 4 中 lista[a] 被計算的次數(shù)為 1000000*10*10,而在優(yōu)化后的代碼中被計算的次數(shù)為 1000000*10,計算次數(shù)大幅度縮短,因此性能有所提升。
8、充分利用 Lazy if-evaluation 的特性
python 中條件表達式是 lazy evaluation 的,也就是說如果存在條件表達式 if x and y,在 x 為 false 的情況下 y 表達式的值將不再計算。因此可以利用該特性在一定程度上提高程序效率。
清單 6. 利用 Lazy if-evaluation 的特性
- from time import time
- t = time()
- abbreviations = ['cf.', 'e.g.', 'ex.', 'etc.', 'fig.', 'i.e.', 'Mr.', 'vs.']
- for i in range (1000000):
- for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
- if w in abbreviations:
- #if w[-1] == '.' and w in abbreviations:
- pass
- print "total run time:"
- print time()-t
在未進行優(yōu)化之前程序的運行時間大概為 8.84,如果使用注釋行代替第一個 if,運行的時間大概為 6.17。
9、字符串的優(yōu)化
python 中的字符串對象是不可改變的,因此對任何字符串的操作如拼接,修改等都將產(chǎn)生一個新的字符串對象,而不是基于原字符串,因此這種持續(xù)的 copy 會在一定程度上影響 python 的性能。對字符串的優(yōu)化也是改善性能的一個重要的方面,特別是在處理文本較多的情況下。字符串的優(yōu)化主要集中在以下幾個方面:
1、在字符串連接的使用盡量使用 join() 而不是 +:在代碼清單 7 中使用 + 進行字符串連接大概需要 0.125 s,而使用 join 縮短為 0.016s。因此在字符的操作上 join 比 + 要快,因此要盡量使用 join 而不是 +。
清單 7. 使用 join 而不是 + 連接字符串
- from time import time
- t = time()
- s = ""
- list = ['a','b','b','d','e','f','g','h','i','j','k','l','m','n']
- for i in range (10000):
- for substr in list:
- s+= substr
- print "total run time:"
- print time()-t
同時要避免:
- s = ""
- for x in list:
- s += func(x)
而是要使用:
- slist = [func(elt) for elt in somelist]
- s = "".join(slist)
2、當對字符串可以使用正則表達式或者內(nèi)置函數(shù)來處理的時候,選擇內(nèi)置函數(shù)。如 str.isalpha(),str.isdigit(),str.startswith((‘x’, ‘yz’)),str.endswith((‘x’, ‘yz’))
3、對字符進行格式化比直接串聯(lián)讀取要快,因此要使用
- out = "%s%s%s%s" % (head, prologue, query, tail)
而避免
- out = "" + head + prologue + query + tail + ""
10、其他優(yōu)化技巧
1、如果需要交換兩個變量的值使用 a,b=b,a 而不是借助中間變量 t=a;a=b;b=t;
- >>> from timeit import Timer
- >>> Timer("t=a;a=b;b=t","a=1;b=2").timeit()
- 0.25154118749729365
- >>> Timer("a,b=b,a","a=1;b=2").timeit()
- 0.17156677734181258
- >>>
2、在循環(huán)的時候使用 xrange 而不是 range。使用 xrange 可以節(jié)省大量的系統(tǒng)內(nèi)存,因為 xrange() 在序列中每次調(diào)用只產(chǎn)生一個整數(shù)元素。而 range() 將直接返回完整的元素列表,用于循環(huán)時會有不必要的開銷。在 python3 中 xrange 不再存在,里面 range 提供一個可以遍歷任意長度的范圍的 iterator。
3、使用局部變量,避免”global” 關(guān)鍵字。python 訪問局部變量會比全局變量要快得多,因 此可以利用這一特性提升性能。
4、if done is not None 比語句 if done != None 更快,讀者可以自行驗證;
5、在耗時較多的循環(huán)中,可以把函數(shù)的調(diào)用改為內(nèi)聯(lián)的方式;
6、使用級聯(lián)比較 “x < y < z” 而不是 “x < y and y < z”;
7、while 1 要比 while True 更快(當然后者的可讀性更好);
8、build in 函數(shù)通常較快,add(a,b) 要優(yōu)于 a+b。
11、定位程序性能瓶頸
對代碼優(yōu)化的前提是需要了解性能瓶頸在什么地方,程序運行的主要時間是消耗在哪里,對于比較復雜的代碼可以借助一些工具來定位,python 內(nèi)置了豐富的性能分析工具,如 profile,cProfile 與 hotshot 等。其中 Profiler 是 python 自帶的一組程序,能夠描述程序運行時候的性能,并提供各種統(tǒng)計幫助用戶定位程序的性能瓶頸。Python 標準模塊提供三種 profilers:cProfile,profile 以及 hotshot。
profile 的使用非常簡單,只需要在使用之前進行 import 即可。具體實例如下:
清單 8. 使用 profile 進行性能分析
- import profile
- def profileTest():
- Total =1;
- for i in range(10):
- Total=Total*(i+1)
- print Total
- return Total
- if __name__ == "__main__":
- profile.run("profileTest()")
程序的運行結(jié)果如下:
- 1
- 2
- 6
- 24
- 120
- 720
- 5040
- 40320
- 362880
- 3628800
- 5 function calls in 0.015 seconds
-
- Ordered by: standard name
-
- ncalls tottime percall cumtime percall filename:lineno(function)
- 1 0.000 0.000 0.000 0.000 :0(range)
- 1 0.015 0.015 0.015 0.015 :0(setprofile)
- 1 0.000 0.000 0.000 0.000 <string>:1(<module>)
- 1 0.000 0.000 0.000 0.000 performance.py:2(profileTest)
- 1 0.000 0.000 0.015 0.015 profile:0(profileTest())
- 0 0.000 0.000 profile:0(profiler)
其中輸出每列的具體解釋如下:
●ncalls:表示函數(shù)調(diào)用的次數(shù);
●tottime:表示指定函數(shù)的總的運行時間,除掉函數(shù)中調(diào)用子函數(shù)的運行時間;
●percall:(第一個 percall)等于 tottime/ncalls;
●cumtime:表示該函數(shù)及其所有子函數(shù)的調(diào)用運行的時間,即函數(shù)開始調(diào)用到返回的時間;
●percall:(第二個 percall)即函數(shù)運行一次的平均時間,等于 cumtime/ncalls;
●filename:lineno(function):每個函數(shù)調(diào)用的具體信息;
如果需要將輸出以日志的形式保存,只需要在調(diào)用的時候加入另外一個參數(shù)。如 profile.run(“profileTest()”,”testprof”)。
對于 profile 的剖析數(shù)據(jù),如果以二進制文件的時候保存結(jié)果的時候,可以通過 pstats 模塊進行文本報表分析,它支持多種形式的報表輸出,是文本界面下一個較為實用的工具。使用非常簡單:
- import pstats
- p = pstats.Stats('testprof')
- p.sort_stats("name").print_stats()
其中 sort_stats() 方法能夠?qū)ζ史謹?shù)據(jù)進行排序, 可以接受多個排序字段,如 sort_stats(‘name’, ‘file’) 將首先按照函數(shù)名稱進行排序,然后再按照文件名進行排序。常見的排序字段有 calls( 被調(diào)用的次數(shù) ),time(函數(shù)內(nèi)部運行時間),cumulative(運行的總時間)等。此外 pstats 也提供了命令行交互工具,執(zhí)行 python – m pstats 后可以通過 help 了解更多使用方式。
對于大型應用程序,如果能夠?qū)⑿阅芊治龅慕Y(jié)果以圖形的方式呈現(xiàn),將會非常實用和直觀,常見的可視化工具有 Gprof2Dot,visualpytune,KCacheGrind 等。
12、性能分析的基本思路
盡管并非每個你寫的Python程序都需要嚴格的性能分析,但了解一下Python的生態(tài)系統(tǒng)中很多優(yōu)秀的在你需要做性能分析的時候可以使用的工具仍然是一件值得去做的事。
分析一個程序的性能,最終都歸結(jié)為回答4個基本的問題:
- 程序運行速度有多快?
- 運行速度瓶頸在哪兒?
- 程序使用了多少內(nèi)存?
- 內(nèi)存泄露發(fā)生在哪里?
下面,我們將使用一些優(yōu)秀的工具深入回答這些問題。
使用time工具粗糙定時
首先,我們可以使用快速然而粗糙的工具:古老的unix工具time,來為我們的代碼檢測運行時間。
- $ time python yourprogram.py
-
- real 0m1.028s
- user 0m0.001s
- sys 0m0.003s
上面三個輸入變量的意義在文章
stackoverflow article 中有詳細介紹。簡單的說:
- real - 表示實際的程序運行時間
- user - 表示程序在用戶態(tài)的cpu總時間
- sys - 表示在內(nèi)核態(tài)的cpu總時間
通過sys和user時間的求和,你可以直觀的得到系統(tǒng)上沒有其他程序運行時你的程序運行所需要的CPU周期。
若sys和user時間之和遠遠少于real時間,那么你可以猜測你的程序的主要性能問題很可能與IO等待相關(guān)。
使用計時上下文管理器進行細粒度計時
我們的下一個技術(shù)涉及訪問細粒度計時信息的直接代碼指令。這是一小段代碼,我發(fā)現(xiàn)使用專門的計時測量是非常重要的:
timer.py
- import time
-
- class Timer(object):
- def __init__(self, verbose=False):
- self.verbose = verbose
-
- def __enter__(self):
- self.start = time.time()
- return self
-
- def __exit__(self, *args):
- self.end = time.time()
- self.secs = self.end - self.start
- self.msecs = self.secs * 1000 # millisecs
- if self.verbose:
- print 'elapsed time: %f ms' % self.msecs
為了使用它,你需要用Python的with關(guān)鍵字和Timer上下文管理器包裝想要計時的代碼塊。它將會在你的代碼塊開始執(zhí)行的時候啟動計時器,在你的代碼塊結(jié)束的時候停止計時器。
這是一個使用上述代碼片段的例子:
- from timer import Timer
- from redis import Redis
- rdb = Redis()
-
- with Timer() as t:
- rdb.lpush("foo", "bar")
- print "=> elasped lpush: %s s" % t.secs
-
- with Timer() as t:
- rdb.lpop("foo")
- print "=> elasped lpop: %s s" % t.secs
我經(jīng)常將這些計時器的輸出記錄到文件中,這樣就可以觀察我的程序的性能如何隨著時間進化。
使用分析器逐行統(tǒng)計時間和執(zhí)行頻率
Robert Kern有一個稱作line_profiler的不錯的項目,我經(jīng)常使用它查看我的腳步中每行代碼多快多頻繁的被執(zhí)行。
想要使用它,你需要通過pip安裝該python包:
- $ pip install line_profiler
一旦安裝完成,你將會使用一個稱做“l(fā)ine_profiler”的新模組和一個“kernprof.py”可執(zhí)行腳本。
想要使用該工具,首先修改你的源代碼,在想要測量的函數(shù)上裝飾@profile裝飾器。不要擔心,你不需要導入任何模組。kernprof.py腳本將會在執(zhí)行的時候?qū)⑺詣拥刈⑷氲侥愕哪_步的運行時。
primes.py
- @profile
- def primes(n):
- if n==2:
- return [2]
- elif n<2:
- return []
- s=range(3,n+1,2)
- mroot = n ** 0.5
- half=(n+1)/2-1
- i=0
- m=3
- while m <= mroot:
- if s[i]:
- j=(m*m-3)/2
- s[j]=0
- while j<half:
- s[j]=0
- j+=m
- i=i+1
- m=2*i+3
- return [2]+[x for x in s if x]
- primes(100)
一旦你已經(jīng)設(shè)置好了@profile裝飾器,使用kernprof.py執(zhí)行你的腳步。
-l選項通知kernprof注入@profile裝飾器到你的腳步的內(nèi)建函數(shù),-v選項通知kernprof在腳本執(zhí)行完畢的時候顯示計時信息。上述腳本的輸出看起來像這樣:
- Wrote profile results to primes.py.lprof
- Timer unit: 1e-06 s
-
- File: primes.py
- Function: primes at line 2
- Total time: 0.00019 s
-
- Line # Hits Time Per Hit % Time Line Contents
- ==============================================================
- 2 @profile
- 3 def primes(n):
- 4 1 2 2.0 1.1 if n==2:
- 5 return [2]
- 6 1 1 1.0 0.5 elif n<2:
- 7 return []
- 8 1 4 4.0 2.1 s=range(3,n+1,2)
- 9 1 10 10.0 5.3 mroot = n ** 0.5
- 10 1 2 2.0 1.1 half=(n+1)/2-1
- 11 1 1 1.0 0.5 i=0
- 12 1 1 1.0 0.5 m=3
- 13 5 7 1.4 3.7 while m <= mroot:
- 14 4 4 1.0 2.1 if s[i]:
- 15 3 4 1.3 2.1 j=(m*m-3)/2
- 16 3 4 1.3 2.1 s[j]=0
- 17 31 31 1.0 16.3 while j<half:
- 18 28 28 1.0 14.7 s[j]=0
- 19 28 29 1.0 15.3 j+=m
- 20 4 4 1.0 2.1 i=i+1
- 21 4 4 1.0 2.1 m=2*i+3
- 22 50 54 1.1 28.4 return [2]+[x for x in s if x]
尋找具有高Hits值或高Time值的行。這些就是可以通過優(yōu)化帶來最大改善的地方。
程序使用了多少內(nèi)存?
現(xiàn)在我們對計時有了較好的理解,那么讓我們繼續(xù)弄清楚程序使用了多少內(nèi)存。我們很幸運,F(xiàn)abian Pedregosa模仿Robert Kern的line_profiler實現(xiàn)了一個不錯的內(nèi)存分析器。
首先使用pip安裝:
- $ pip install -U memory_profiler
- $ pip install psutil
(這里建議安裝psutil包,因為它可以大大改善memory_profiler的性能)。
就像line_profiler,memory_profiler也需要在感興趣的函數(shù)上面裝飾@profile裝飾器:
- @profile
- def primes(n):
- ...
- ...
想要觀察你的函數(shù)使用了多少內(nèi)存,像下面這樣執(zhí)行:
- $ python -m memory_profiler primes.py
一旦程序退出,你將會看到看起來像這樣的輸出:
- Filename: primes.py
-
- Line # Mem usage Increment Line Contents
- ==============================================
- 2 @profile
- 3 7.9219 MB 0.0000 MB def primes(n):
- 4 7.9219 MB 0.0000 MB if n==2:
- 5 return [2]
- 6 7.9219 MB 0.0000 MB elif n<2:
- 7 return []
- 8 7.9219 MB 0.0000 MB s=range(3,n+1,2)
- 9 7.9258 MB 0.0039 MB mroot = n ** 0.5
- 10 7.9258 MB 0.0000 MB half=(n+1)/2-1
- 11 7.9258 MB 0.0000 MB i=0
- 12 7.9258 MB 0.0000 MB m=3
- 13 7.9297 MB 0.0039 MB while m <= mroot:
- 14 7.9297 MB 0.0000 MB if s[i]:
- 15 7.9297 MB 0.0000 MB j=(m*m-3)/2
- 16 7.9258 MB -0.0039 MB s[j]=0
- 17 7.9297 MB 0.0039 MB while j<half:
- 18 7.9297 MB 0.0000 MB s[j]=0
- 19 7.9297 MB 0.0000 MB j+=m
- 20 7.9297 MB 0.0000 MB i=i+1
- 21 7.9297 MB 0.0000 MB m=2*i+3
- 22 7.9297 MB 0.0000 MB return [2]+[x for x in s if x]
line_profiler和memory_profiler的IPython快捷方式
memory_profiler和line_profiler有一個鮮為人知的小竅門,兩者都有在IPython中的快捷命令。你需要做的就是在IPython會話中輸入以下內(nèi)容:
- %load_ext memory_profiler
- %load_ext line_profiler
在這樣做的時候你需要訪問魔法命令%lprun和%mprun,它們的行為類似于他們的命令行形式。主要區(qū)別是你不需要使用@profiledecorator來修飾你要分析的函數(shù)。只需要在IPython會話中像先前一樣直接運行分析:
- In [1]: from primes import primes
- In [2]: %mprun -f primes primes(1000)
- In [3]: %lprun -f primes primes(1000)
這樣可以節(jié)省你很多時間和精力,因為你的源代碼不需要為使用這些分析命令而進行修改。
內(nèi)存泄漏在哪里?
cPython解釋器使用引用計數(shù)做為記錄內(nèi)存使用的主要方法。這意味著每個對象包含一個計數(shù)器,當某處對該對象的引用被存儲時計數(shù)器增加,當引用被刪除時計數(shù)器遞減。當計數(shù)器到達零時,cPython解釋器就知道該對象不再被使用,所以刪除對象,釋放占用的內(nèi)存。
如果程序中不再被使用的對象的引用一直被占有,那么就經(jīng)常發(fā)生內(nèi)存泄漏。
查找這種“內(nèi)存泄漏”最快的方式是使用Marius Gedminas編寫的objgraph,這是一個極好的工具。該工具允許你查看內(nèi)存中對象的數(shù)量,定位含有該對象的引用的所有代碼的位置。
一開始,首先安裝objgraph:
一旦你已經(jīng)安裝了這個工具,在你的代碼中插入一行聲明調(diào)用調(diào)試器:
- import pdb; pdb.set_trace()
最普遍的對象是哪些?
在運行的時候,你可以通過執(zhí)行下述指令查看程序中前20個最普遍的對象:
- (pdb) import objgraph
- (pdb) objgraph.show_most_common_types()
-
- MyBigFatObject 20000
- tuple 16938
- function 4310
- dict 2790
- wrapper_descriptor 1181
- builtin_function_or_method 934
- weakref 764
- list 634
- method_descriptor 507
- getset_descriptor 451
- type 439
哪些對象已經(jīng)被添加或刪除?
我們也可以查看兩個時間點之間那些對象已經(jīng)被添加或刪除:
- (pdb) import objgraph
- (pdb) objgraph.show_growth()
- .
- .
- .
- (pdb) objgraph.show_growth() # this only shows objects that has been added or deleted since last show_growth() call
-
- traceback 4 +2
- KeyboardInterrupt 1 +1
- frame 24 +1
- list 667 +1
- tuple 16969 +1
誰引用著泄漏的對象?
繼續(xù),你還可以查看哪里包含給定對象的引用。讓我們以下述簡單的程序做為一個例子:
- x = [1]
- y = [x, [x], {"a":x}]
- import pdb; pdb.set_trace()
想要看看哪里包含變量x的引用,執(zhí)行objgraph.show_backref()函數(shù):
- (pdb) import objgraph
- (pdb) objgraph.show_backref([x], filename="/tmp/backrefs.png")
該命令的輸出應該是一副PNG圖像,保存在/tmp/backrefs.png,它看起來是像這樣:
最下面有紅字的盒子是我們感興趣的對象。我們可以看到,它被符號x引用了一次,被列表y引用了三次。如果是x引起了一個內(nèi)存泄漏,我們可以使用這個方法,通過跟蹤它的所有引用,來檢查為什么它沒有自動的被釋放。
回顧一下,objgraph 使我們可以:
- 顯示占據(jù)python程序內(nèi)存的頭N個對象
- 顯示一段時間以后哪些對象被刪除活增加了
- 在我們的腳本中顯示某個給定對象的所有引用
努力與精度
在本帖中,我給你顯示了怎樣用幾個工具來分析python程序的性能。通過這些工具與技術(shù)的武裝,你可以獲得所有需要的信息,來跟蹤一個python程序中大多數(shù)的內(nèi)存泄漏,以及識別出其速度瓶頸。
對許多其他觀點來說,運行一次性能分析就意味著在努力目標與事實精度之間做出平衡。如果感到困惑,那么就實現(xiàn)能適應你目前需求的最簡單的解決方案。
參考文章:
http:///blog/speeding-up-your-python-code/
https://www.ibm.com/developerworks/cn/linux/l-cn-python-optim/
http://www./posts/python-performance-analysis/
|