1、使用生成器和列表解析
一個(gè)普遍被忽略的內(nèi)存優(yōu)化是生成器的使用。生成器讓我們創(chuàng)建一個(gè)函數(shù)一次只返回一條記錄,而不是一次返回所有的記錄,如果你正在使用python2.x,這就是你為啥使用xrange替代range或者使用ifilter替代filter的原因。一個(gè)很好地例子就是創(chuàng)建一個(gè)很大的列表并將它們拼合在一起。
- 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)建一個(gè)新的 list 更為高效,因此我們可以利用這一特性來(lái)提高運(yùn)行的效率。
- 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]
上述代碼直接運(yùn)行大概需要 17s,而改為使用列表解析后 ,運(yùn)行時(shí)間縮短為 9.29s。將近提高了一半。生成器表達(dá)式則是在 2.4 中引入的新內(nèi)容,語(yǔ)法和列表解析類似,但是在大數(shù)據(jù)量處理時(shí),生成器表達(dá)式的優(yōu)勢(shì)較為明顯,它并不創(chuàng)建一個(gè)列表,只是返回一個(gè)生成器,因此效率較高。在上述例子上中代碼 a = [w for w in list] 修改為 a = (w for w in list),運(yùn)行時(shí)間進(jìn)一步減少,縮短約為 2.98s。
2、Ctypes的介紹
對(duì)于關(guān)鍵性的性能代碼python本身也提供給我們一個(gè)API來(lái)調(diào)用C方法,主要通過(guò) ctypes來(lái)實(shí)現(xiàn),你可以不寫任何C代碼來(lái)利用ctypes。默認(rèn)情況下python提供了預(yù)編譯的標(biāo)準(zhǔn)c庫(kù),我們?cè)倩氐缴善鞯睦樱纯词褂胏types實(shí)現(xiàn)花費(fèi)多少時(shí)間。
- 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的隨機(jī)函數(shù),運(yùn)行時(shí)間減了大半!現(xiàn)在如果我告訴你我們還能做得更好,你信嗎?
3、Cython的介紹
Cython 是python的一個(gè)超集,允許我們調(diào)用C函數(shù)以及聲明變量來(lái)提高性能。嘗試使用之前我們需要先安裝Cython.
Cython 本質(zhì)上是另一個(gè)不再開(kāi)發(fā)的類似類庫(kù)Pyrex的分支,它將我們的類Python代碼編譯成C庫(kù),我們可以在一個(gè)python文件中調(diào)用。對(duì)于你的python文件使用.pyx后綴替代.py后綴,讓我們看一下使用Cython如何來(lái)運(yùn)行我們的生成器代碼。
- #cython_generator.pyx
- import random
-
- def generate(num):
- while num:
- yield random.randrange(10)
- num -= 1
我們需要?jiǎng)?chuàng)建個(gè)setup.py以便我們能獲取到Cython來(lái)編譯我們的函數(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
你應(yīng)該可以看到兩個(gè)文件cython_generator.c 文件和generator.so文件,我們使用下面方法測(cè)試我們的程序:
- import timeit
- print(timeit.timeit("sum(generator.generate(999))", setup="import generator", number=1000))
- >>> 0.835658073425
還不賴,讓我們看看是否還有可以改進(jìn)的地方。我們可以先聲明“num”為整形,接著我們可以導(dǎo)入標(biāo)準(zhǔn)的C庫(kù)來(lái)負(fù)責(zé)我們的隨機(jī)函數(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
如果我們?cè)俅尉幾g運(yùn)行我們會(huì)看到這一串驚人的數(shù)字。
僅僅的幾個(gè)改變帶來(lái)了不賴的結(jié)果。然而,有時(shí)這個(gè)改變很乏味,因此讓我們來(lái)看看如何使用規(guī)則的python來(lái)實(shí)現(xiàn)吧。
4、PyPy的介紹
PyPy 是一個(gè)Python2.7.3的即時(shí)編譯器(JIT),通俗地說(shuō)這意味著讓你的代碼運(yùn)行的更快。Quora在生產(chǎn)環(huán)境中使用了PyPy。PyPy在它們的下載頁(yè)面有一些安裝說(shuō)明,但是如果你使用的Ubuntu系統(tǒng),你可以通過(guò)apt-get來(lái)安裝。它的運(yùn)行方式是立即可用的,因此沒(méi)有瘋狂的bash或者運(yùn)行腳本,只需下載然后運(yùn)行即可。讓我們看看我們?cè)嫉纳善鞔a在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、進(jìn)一步測(cè)試
為什么還要進(jìn)一步研究?PyPy是冠軍!并不全對(duì)。雖然大多數(shù)程序可以運(yùn)行在PyPy上,但是還是有一些庫(kù)沒(méi)有被完全支持。而且,為你的項(xiàng)目寫C的擴(kuò)展相比換一個(gè)編譯器更加容易。讓我們更加深入一些,看看ctypes如何讓我們使用C來(lái)寫庫(kù)。我們來(lái)測(cè)試一下歸并排序和計(jì)算斐波那契數(shù)列的速度。下面是我們要用到的C代碼(functions.c):
- /* functions.c */
- #include "stdio.h"
- #include "stdlib.h"
- #include "string.h"
-
- /* http://rosettacode.org/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平臺(tái),我們可以用下面的方法把它編譯成一個(gè)共享庫(kù):
gcc
-
Wall
-
fPIC
-
c functions.c
gcc
-
shared
-
o libfunctions.so functions.o
使用ctypes, 通過(guò)加載”libfunctions.so”這個(gè)共享庫(kù),就像我們前邊對(duì)標(biāo)準(zhǔn)C庫(kù)所作的那樣,就可以使用這個(gè)庫(kù)了。這里我們將要比較Python實(shí)現(xiàn)和C實(shí)現(xiàn)。現(xiàn)在我們開(kāi)始計(jì)算斐波那契數(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
正如我們預(yù)料的那樣,C比Python和PyPy更快。我們也可以用同樣的方式比較歸并排序。
我們還沒(méi)有深挖Cypes庫(kù),所以這些例子并沒(méi)有反映python強(qiáng)大的一面,Cypes庫(kù)只有少量的標(biāo)準(zhǔn)類型限制,比如int型,char數(shù)組,float型,字節(jié)(bytes)等等。默認(rèn)情況下,沒(méi)有整形數(shù)組,然而通過(guò)與c_int相乘(ctype為int類型)我們可以間接獲得這樣的數(shù)組。這也是代碼第7行所要呈現(xiàn)的。我們創(chuàng)建了一個(gè)c_int數(shù)組,有關(guān)我們數(shù)字的數(shù)組并分解打包到c_int數(shù)組中
主要的是c語(yǔ)言不能這樣做,而且你也不想。我們用指針來(lái)修改函數(shù)體。為了通過(guò)我們的c_numbers的數(shù)列,我們必須通過(guò)引用傳遞merge_sort功能。運(yùn)行merge_sort后,我們利用c_numbers數(shù)組進(jìn)行排序,我已經(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
這兒通過(guò)表格和圖標(biāo)來(lái)比較不同的結(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)化能夠讓程序運(yùn)行更快,它是在不改變程序運(yùn)行結(jié)果的情況下使得程序的運(yùn)行效率更高,根據(jù) 80/20 原則,實(shí)現(xiàn)程序的重構(gòu)、優(yōu)化、擴(kuò)展以及文檔相關(guān)的事情通常需要消耗 80% 的工作量。優(yōu)化通常包含兩方面的內(nèi)容:減小代碼的體積,提高代碼的運(yùn)行效率。
6、改進(jìn)算法,選擇合適的數(shù)據(jù)結(jié)構(gòu)
一個(gè)良好的算法能夠?qū)π阅芷鸬疥P(guān)鍵作用,因此性能改進(jìn)的首要點(diǎn)是對(duì)算法的改進(jìn)。在算法的時(shí)間復(fù)雜度排序上依次是:
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
因此如果能夠在時(shí)間復(fù)雜度上對(duì)算法進(jìn)行一定的改進(jìn),對(duì)性能的提高不言而喻。但對(duì)具體算法的改進(jìn)不屬于本文討論的范圍,讀者可以自行參考這方面資料。下面的內(nèi)容將集中討論數(shù)據(jù)結(jié)構(gòu)的選擇。
● 字典 (dictionary) 與列表 (list)
Python 字典中使用了hash table,因此查找操作的復(fù)雜度為 O(1),而 list 實(shí)際是個(gè)數(shù)組,在 list 中,查找需要遍歷整個(gè) list,其復(fù)雜度為 O(n),因此對(duì)成員的查找訪問(wè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
上述代碼運(yùn)行大概需要 16.09seconds。如果去掉行 #list = dict.fromkeys(list,True) 的注釋,將 list 轉(zhuǎn)換為字典之后再運(yùn)行,時(shí)間大約為 8.375 seconds,效率大概提高了一半。因此在需要多數(shù)據(jù)成員進(jìn)行頻繁的查找或者訪問(wèn)的時(shí)候,使用 dict 而不是 list 是一個(gè)較好的選擇。
● 集合 (set) 與列表 (list)
set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的問(wèn)題可以轉(zhuǎn)換為 set 來(lái)操作。
清單 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
上述程序的運(yùn)行時(shí)間大概為:
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 后程序的運(yùn)行時(shí)間縮減為 8.75,提高了 4 倍多,運(yùn)行時(shí)間大大縮短。讀者可以自行使用表 1 其他的操作進(jìn)行測(cè)試。
表 1. set 常見(jiàn)用法
語(yǔ)法 | 操作 | 說(shuō)明 |
---|
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、對(duì)循環(huán)的優(yōu)化
對(duì)循環(huán)的優(yōu)化所遵循的原則是盡量減少循環(huán)過(guò)程中的計(jì)算量,有多重循環(huán)的盡量將內(nèi)層的計(jì)算提到上一層。 下面通過(guò)實(shí)例來(lái)對(duì)比循環(huán)優(yōu)化后所帶來(lái)的性能的提高。程序清單 4 中,如果不進(jìn)行循環(huán)優(yōu)化,其大概的運(yùn)行時(shí)間約為 132.375。
清單 4. 為進(jìn)行循環(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)在進(jìn)行如下優(yōu)化,將長(zhǎng)度計(jì)算提到循環(huán)外,range 用 xrange 代替,同時(shí)將第三層的計(jì)算 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)化后的程序其運(yùn)行時(shí)間縮短為 102.171999931。在清單 4 中 lista[a] 被計(jì)算的次數(shù)為 1000000*10*10,而在優(yōu)化后的代碼中被計(jì)算的次數(shù)為 1000000*10,計(jì)算次數(shù)大幅度縮短,因此性能有所提升。
8、充分利用 Lazy if-evaluation 的特性
python 中條件表達(dá)式是 lazy evaluation 的,也就是說(shuō)如果存在條件表達(dá)式 if x and y,在 x 為 false 的情況下 y 表達(dá)式的值將不再計(jì)算。因此可以利用該特性在一定程度上提高程序效率。
清單 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
在未進(jìn)行優(yōu)化之前程序的運(yùn)行時(shí)間大概為 8.84,如果使用注釋行代替第一個(gè) if,運(yùn)行的時(shí)間大概為 6.17。
9、字符串的優(yōu)化
python 中的字符串對(duì)象是不可改變的,因此對(duì)任何字符串的操作如拼接,修改等都將產(chǎn)生一個(gè)新的字符串對(duì)象,而不是基于原字符串,因此這種持續(xù)的 copy 會(huì)在一定程度上影響 python 的性能。對(duì)字符串的優(yōu)化也是改善性能的一個(gè)重要的方面,特別是在處理文本較多的情況下。字符串的優(yōu)化主要集中在以下幾個(gè)方面:
1、在字符串連接的使用盡量使用 join() 而不是 +:在代碼清單 7 中使用 + 進(jìn)行字符串連接大概需要 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
同時(shí)要避免:
- s = ""
- for x in list:
- s += func(x)
而是要使用:
- slist = [func(elt) for elt in somelist]
- s = "".join(slist)
2、當(dāng)對(duì)字符串可以使用正則表達(dá)式或者內(nèi)置函數(shù)來(lái)處理的時(shí)候,選擇內(nèi)置函數(shù)。如 str.isalpha(),str.isdigit(),str.startswith((‘x’, ‘yz’)),str.endswith((‘x’, ‘yz’))
3、對(duì)字符進(jìn)行格式化比直接串聯(lián)讀取要快,因此要使用
- out = "%s%s%s%s" % (head, prologue, query, tail)
而避免
- out = "" + head + prologue + query + tail + ""
10、其他優(yōu)化技巧
1、如果需要交換兩個(gè)變量的值使用 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)的時(shí)候使用 xrange 而不是 range。使用 xrange 可以節(jié)省大量的系統(tǒng)內(nèi)存,因?yàn)?xrange() 在序列中每次調(diào)用只產(chǎn)生一個(gè)整數(shù)元素。而 range() 將直接返回完整的元素列表,用于循環(huán)時(shí)會(huì)有不必要的開(kāi)銷。在 python3 中 xrange 不再存在,里面 range 提供一個(gè)可以遍歷任意長(zhǎng)度的范圍的 iterator。
3、使用局部變量,避免”global” 關(guān)鍵字。python 訪問(wèn)局部變量會(huì)比全局變量要快得多,因 此可以利用這一特性提升性能。
4、if done is not None 比語(yǔ)句 if done != None 更快,讀者可以自行驗(yàn)證;
5、在耗時(shí)較多的循環(huán)中,可以把函數(shù)的調(diào)用改為內(nèi)聯(lián)的方式;
6、使用級(jí)聯(lián)比較 “x < y < z” 而不是 “x < y and y < z”;
7、while 1 要比 while True 更快(當(dāng)然后者的可讀性更好);
8、build in 函數(shù)通常較快,add(a,b) 要優(yōu)于 a+b。
11、定位程序性能瓶頸
對(duì)代碼優(yōu)化的前提是需要了解性能瓶頸在什么地方,程序運(yùn)行的主要時(shí)間是消耗在哪里,對(duì)于比較復(fù)雜的代碼可以借助一些工具來(lái)定位,python 內(nèi)置了豐富的性能分析工具,如 profile,cProfile 與 hotshot 等。其中 Profiler 是 python 自帶的一組程序,能夠描述程序運(yùn)行時(shí)候的性能,并提供各種統(tǒng)計(jì)幫助用戶定位程序的性能瓶頸。Python 標(biāo)準(zhǔn)模塊提供三種 profilers:cProfile,profile 以及 hotshot。
profile 的使用非常簡(jiǎn)單,只需要在使用之前進(jìn)行 import 即可。具體實(shí)例如下:
清單 8. 使用 profile 進(jìn)行性能分析
- 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()")
程序的運(yùn)行結(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ù)的總的運(yùn)行時(shí)間,除掉函數(shù)中調(diào)用子函數(shù)的運(yùn)行時(shí)間;
●percall:(第一個(gè) percall)等于 tottime/ncalls;
●cumtime:表示該函數(shù)及其所有子函數(shù)的調(diào)用運(yùn)行的時(shí)間,即函數(shù)開(kāi)始調(diào)用到返回的時(shí)間;
●percall:(第二個(gè) percall)即函數(shù)運(yùn)行一次的平均時(shí)間,等于 cumtime/ncalls;
●filename:lineno(function):每個(gè)函數(shù)調(diào)用的具體信息;
如果需要將輸出以日志的形式保存,只需要在調(diào)用的時(shí)候加入另外一個(gè)參數(shù)。如 profile.run(“profileTest()”,”testprof”)。
對(duì)于 profile 的剖析數(shù)據(jù),如果以二進(jìn)制文件的時(shí)候保存結(jié)果的時(shí)候,可以通過(guò) pstats 模塊進(jìn)行文本報(bào)表分析,它支持多種形式的報(bào)表輸出,是文本界面下一個(gè)較為實(shí)用的工具。使用非常簡(jiǎn)單:
- import pstats
- p = pstats.Stats('testprof')
- p.sort_stats("name").print_stats()
其中 sort_stats() 方法能夠?qū)ζ史謹(jǐn)?shù)據(jù)進(jìn)行排序, 可以接受多個(gè)排序字段,如 sort_stats(‘name’, ‘file’) 將首先按照函數(shù)名稱進(jìn)行排序,然后再按照文件名進(jìn)行排序。常見(jiàn)的排序字段有 calls( 被調(diào)用的次數(shù) ),time(函數(shù)內(nèi)部運(yùn)行時(shí)間),cumulative(運(yùn)行的總時(shí)間)等。此外 pstats 也提供了命令行交互工具,執(zhí)行 python – m pstats 后可以通過(guò) help 了解更多使用方式。
對(duì)于大型應(yīng)用程序,如果能夠?qū)⑿阅芊治龅慕Y(jié)果以圖形的方式呈現(xiàn),將會(huì)非常實(shí)用和直觀,常見(jiàn)的可視化工具有 Gprof2Dot,visualpytune,KCacheGrind 等。
12、性能分析的基本思路
盡管并非每個(gè)你寫的Python程序都需要嚴(yán)格的性能分析,但了解一下Python的生態(tài)系統(tǒng)中很多優(yōu)秀的在你需要做性能分析的時(shí)候可以使用的工具仍然是一件值得去做的事。
分析一個(gè)程序的性能,最終都?xì)w結(jié)為回答4個(gè)基本的問(wèn)題:
- 程序運(yùn)行速度有多快?
- 運(yùn)行速度瓶頸在哪兒?
- 程序使用了多少內(nèi)存?
- 內(nèi)存泄露發(fā)生在哪里?
下面,我們將使用一些優(yōu)秀的工具深入回答這些問(wèn)題。使用time工具粗糙定時(shí)
首先,我們可以使用快速然而粗糙的工具:古老的unix工具time,來(lái)為我們的代碼檢測(cè)運(yùn)行時(shí)間。
- $ time python yourprogram.py
-
- real 0m1.028s
- user 0m0.001s
- sys 0m0.003s
上面三個(gè)輸入變量的意義在文章
stackoverflow article 中有詳細(xì)介紹。簡(jiǎn)單的說(shuō):
- real - 表示實(shí)際的程序運(yùn)行時(shí)間
- user - 表示程序在用戶態(tài)的cpu總時(shí)間
- sys - 表示在內(nèi)核態(tài)的cpu總時(shí)間
通過(guò)sys和user時(shí)間的求和,你可以直觀的得到系統(tǒng)上沒(méi)有其他程序運(yùn)行時(shí)你的程序運(yùn)行所需要的CPU周期。
若sys和user時(shí)間之和遠(yuǎn)遠(yuǎn)少于real時(shí)間,那么你可以猜測(cè)你的程序的主要性能問(wèn)題很可能與IO等待相關(guān)。
使用計(jì)時(shí)上下文管理器進(jìn)行細(xì)粒度計(jì)時(shí)
我們的下一個(gè)技術(shù)涉及訪問(wèn)細(xì)粒度計(jì)時(shí)信息的直接代碼指令。這是一小段代碼,我發(fā)現(xiàn)使用專門的計(jì)時(shí)測(cè)量是非常重要的:
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上下文管理器包裝想要計(jì)時(shí)的代碼塊。它將會(huì)在你的代碼塊開(kāi)始執(zhí)行的時(shí)候啟動(dòng)計(jì)時(shí)器,在你的代碼塊結(jié)束的時(shí)候停止計(jì)時(shí)器。
這是一個(gè)使用上述代碼片段的例子:
- 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)常將這些計(jì)時(shí)器的輸出記錄到文件中,這樣就可以觀察我的程序的性能如何隨著時(shí)間進(jìn)化。
使用分析器逐行統(tǒng)計(jì)時(shí)間和執(zhí)行頻率
Robert Kern有一個(gè)稱作line_profiler的不錯(cuò)的項(xiàng)目,我經(jīng)常使用它查看我的腳步中每行代碼多快多頻繁的被執(zhí)行。
想要使用它,你需要通過(guò)pip安裝該python包:
- $ pip install line_profiler
一旦安裝完成,你將會(huì)使用一個(gè)稱做“l(fā)ine_profiler”的新模組和一個(gè)“kernprof.py”可執(zhí)行腳本。
想要使用該工具,首先修改你的源代碼,在想要測(cè)量的函數(shù)上裝飾@profile裝飾器。不要擔(dān)心,你不需要導(dǎo)入任何模組。kernprof.py腳本將會(huì)在執(zhí)行的時(shí)候?qū)⑺詣?dòng)地注入到你的腳步的運(yùn)行時(shí)。
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選項(xiàng)通知kernprof注入@profile裝飾器到你的腳步的內(nèi)建函數(shù),-v選項(xiàng)通知kernprof在腳本執(zhí)行完畢的時(shí)候顯示計(jì)時(shí)信息。上述腳本的輸出看起來(lái)像這樣:
- 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值的行。這些就是可以通過(guò)優(yōu)化帶來(lái)最大改善的地方。程序使用了多少內(nèi)存?
現(xiàn)在我們對(duì)計(jì)時(shí)有了較好的理解,那么讓我們繼續(xù)弄清楚程序使用了多少內(nèi)存。我們很幸運(yùn),F(xiàn)abian Pedregosa模仿Robert Kern的line_profiler實(shí)現(xiàn)了一個(gè)不錯(cuò)的內(nèi)存分析器。
首先使用pip安裝:
- $ pip install -U memory_profiler
- $ pip install psutil
(這里建議安裝psutil包,因?yàn)樗梢源蟠蟾纳苖emory_profiler的性能)。
就像line_profiler,memory_profiler也需要在感興趣的函數(shù)上面裝飾@profile裝飾器:
- @profile
- def primes(n):
- ...
- ...
想要觀察你的函數(shù)使用了多少內(nèi)存,像下面這樣執(zhí)行:
- $ python -m memory_profiler primes.py
一旦程序退出,你將會(huì)看到看起來(lái)像這樣的輸出:
- 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有一個(gè)鮮為人知的小竅門,兩者都有在IPython中的快捷命令。你需要做的就是在IPython會(huì)話中輸入以下內(nèi)容:
- %load_ext memory_profiler
- %load_ext line_profiler
在這樣做的時(shí)候你需要訪問(wèn)魔法命令%lprun和%mprun,它們的行為類似于他們的命令行形式。主要區(qū)別是你不需要使用@profiledecorator來(lái)修飾你要分析的函數(shù)。只需要在IPython會(huì)話中像先前一樣直接運(yùn)行分析:
- In [1]: from primes import primes
- In [2]: %mprun -f primes primes(1000)
- In [3]: %lprun -f primes primes(1000)
這樣可以節(jié)省你很多時(shí)間和精力,因?yàn)槟愕脑创a不需要為使用這些分析命令而進(jìn)行修改。
內(nèi)存泄漏在哪里?
cPython解釋器使用引用計(jì)數(shù)做為記錄內(nèi)存使用的主要方法。這意味著每個(gè)對(duì)象包含一個(gè)計(jì)數(shù)器,當(dāng)某處對(duì)該對(duì)象的引用被存儲(chǔ)時(shí)計(jì)數(shù)器增加,當(dāng)引用被刪除時(shí)計(jì)數(shù)器遞減。當(dāng)計(jì)數(shù)器到達(dá)零時(shí),cPython解釋器就知道該對(duì)象不再被使用,所以刪除對(duì)象,釋放占用的內(nèi)存。
如果程序中不再被使用的對(duì)象的引用一直被占有,那么就經(jīng)常發(fā)生內(nèi)存泄漏。
查找這種“內(nèi)存泄漏”最快的方式是使用Marius Gedminas編寫的objgraph,這是一個(gè)極好的工具。該工具允許你查看內(nèi)存中對(duì)象的數(shù)量,定位含有該對(duì)象的引用的所有代碼的位置。
一開(kāi)始,首先安裝objgraph:
一旦你已經(jīng)安裝了這個(gè)工具,在你的代碼中插入一行聲明調(diào)用調(diào)試器:
- import pdb; pdb.set_trace()
最普遍的對(duì)象是哪些?
在運(yùn)行的時(shí)候,你可以通過(guò)執(zhí)行下述指令查看程序中前20個(gè)最普遍的對(duì)象:
- (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
哪些對(duì)象已經(jīng)被添加或刪除?
我們也可以查看兩個(gè)時(shí)間點(diǎn)之間那些對(duì)象已經(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
誰(shuí)引用著泄漏的對(duì)象?
繼續(xù),你還可以查看哪里包含給定對(duì)象的引用。讓我們以下述簡(jiǎn)單的程序做為一個(gè)例子:
- 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")
該命令的輸出應(yīng)該是一副PNG圖像,保存在/tmp/backrefs.png,它看起來(lái)是像這樣:
最下面有紅字的盒子是我們感興趣的對(duì)象。我們可以看到,它被符號(hào)x引用了一次,被列表y引用了三次。如果是x引起了一個(gè)內(nèi)存泄漏,我們可以使用這個(gè)方法,通過(guò)跟蹤它的所有引用,來(lái)檢查為什么它沒(méi)有自動(dòng)的被釋放。
回顧一下,objgraph 使我們可以:
- 顯示占據(jù)python程序內(nèi)存的頭N個(gè)對(duì)象
- 顯示一段時(shí)間以后哪些對(duì)象被刪除活增加了
- 在我們的腳本中顯示某個(gè)給定對(duì)象的所有引用
努力與精度
在本帖中,我給你顯示了怎樣用幾個(gè)工具來(lái)分析python程序的性能。通過(guò)這些工具與技術(shù)的武裝,你可以獲得所有需要的信息,來(lái)跟蹤一個(gè)python程序中大多數(shù)的內(nèi)存泄漏,以及識(shí)別出其速度瓶頸。
對(duì)許多其他觀點(diǎn)來(lái)說(shuō),運(yùn)行一次性能分析就意味著在努力目標(biāo)與事實(shí)精度之間做出平衡。如果感到困惑,那么就實(shí)現(xiàn)能適應(yīng)你目前需求的最簡(jiǎn)單的解決方案。
參考文章:
http://maxburstein.com/blog/speeding-up-your-python-code/
https://www.ibm.com/developerworks/cn/linux/l-cn-python-optim/
http://www.huyng.com/posts/python-performance-analysis/