1、使用生成器和列表解析
一個普遍被忽略的內存優化是生成器的使用。生成器讓我們創建一個函數一次只返回一條記錄,而不是一次返回所有的記錄,如果你正在使用python2.x,這就是你爲啥使用xrange替代range或者使用ifilter替代filter的原因。一個很好地例子就是創建一個很大的列表並將它們拼合在一起。
- 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
列表解析要比在循環中重新構建一個新的 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]
2、Ctypes的介紹
對於關鍵性的性能代碼python本身也提供給我們一個API來調用C方法,主要通過 ctypes來實現,你可以不寫任何C代碼來利用ctypes。默認情況下python提供了預編譯的標準c庫,我們再回到生成器的例子,看看使用ctypes實現花費多少時間。
- 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的隨機函數,運行時間減了大半!現在如果我告訴你我們還能做得更好,你信嗎?
3、Cython的介紹
Cython 是python的一個超集,允許我們調用C函數以及聲明變量來提高性能。嘗試使用之前我們需要先安裝Cython.
sudo pip install cython
Cython 本質上是另一個不再開發的類似類庫Pyrex的分支,它將我們的類Python代碼編譯成C庫,我們可以在一個python文件中調用。對於你的python文件使用.pyx後綴替代.py後綴,讓我們看一下使用Cython如何來運行我們的生成器代碼。
- #cython_generator.pyx
- import random
- def generate(num):
- while num:
- yield random.randrange(10)
- num -= 1
- 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
- #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
>>>
0.033586025238
4、PyPy的介紹
PyPy 是一個Python2.7.3的即時編譯器(JIT),通俗地說這意味着讓你的代碼運行的更快。Quora在生產環境中使用了PyPy。PyPy在它們的下載頁面有一些安裝說明,但是如果你使用的Ubuntu系統,你可以通過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是冠軍!並不全對。雖然大多數程序可以運行在PyPy上,但是還是有一些庫沒有被完全支持。而且,爲你的項目寫C的擴展相比換一個編譯器更加容易。讓我們更加深入一些,看看ctypes如何讓我們使用C來寫庫。我們來測試一下歸併排序和計算斐波那契數列的速度。下面是我們要用到的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);
- }
gcc
-
Wall
-
fPIC
-
c
functions.c
gcc
-
shared
-
o libfunctions.so functions.o
使用ctypes, 通過加載”libfunctions.so”這個共享庫,就像我們前邊對標準C庫所作的那樣,就可以使用這個庫了。這裏我們將要比較Python實現和C實現。現在我們開始計算斐波那契數列:
- #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數組,float型,字節(bytes)等等。默認情況下,沒有整形數組,然而通過與c_int相乘(ctype爲int類型)我們可以間接獲得這樣的數組。這也是代碼第7行所要呈現的。我們創建了一個c_int數組,有關我們數字的數組並分解打包到c_int數組中
主要的是c語言不能這樣做,而且你也不想。我們用指針來修改函數體。爲了通過我們的c_numbers的數列,我們必須通過引用傳遞merge_sort功能。運行merge_sort後,我們利用c_numbers數組進行排序,我已經把下面的代碼加到我的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
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 |
代碼優化能夠讓程序運行更快,它是在不改變程序運行結果的情況下使得程序的運行效率更高,根據 80/20 原則,實現程序的重構、優化、擴展以及文檔相關的事情通常需要消耗 80% 的工作量。優化通常包含兩方面的內容:減小代碼的體積,提高代碼的運行效率。
6、改進算法,選擇合適的數據結構
一個良好的算法能夠對性能起到關鍵作用,因此性能改進的首要點是對算法的改進。在算法的時間複雜度排序上依次是:
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
因此如果能夠在時間複雜度上對算法進行一定的改進,對性能的提高不言而喻。但對具體算法的改進不屬於本文討論的範圍,讀者可以自行參考這方面資料。下面的內容將集中討論數據結構的選擇。
● 字典 (dictionary) 與列表 (list)
Python 字典中使用了hash table,因此查找操作的複雜度爲 O(1),而 list 實際是個數組,在 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 轉換爲字典之後再運行,時間大約爲 8.375 seconds,效率大概提高了一半。因此在需要多數據成員進行頻繁的查找或者訪問的時候,使用 dict 而不是 list 是一個較好的選擇。
● 集合 (set) 與列表 (list)
set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,並集或者差的問題可以轉換爲 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 所有數據的新集合 |
set(list1) & set(list2) | intersection | 包含 list1 和 list2 中共同元素的新集合 |
set(list1) - set(list2) | difference | 在 list1 中出現但不在 list2 中出現的元素的集合 |
7、對循環的優化
對循環的優化所遵循的原則是儘量減少循環過程中的計算量,有多重循環的儘量將內層的計算提到上一層。 下面通過實例來對比循環優化後所帶來的性能的提高。程序清單 4 中,如果不進行循環優化,其大概的運行時間約爲 132.375。
清單 4. 爲進行循環優化前
- 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
現在進行如下優化,將長度計算提到循環外,range 用 xrange 代替,同時將第三層的計算 lista[a] 提到循環的第二層。
清單 5. 循環優化後
- 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
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
在未進行優化之前程序的運行時間大概爲 8.84,如果使用註釋行代替第一個 if,運行的時間大概爲 6.17。
9、字符串的優化
python 中的字符串對象是不可改變的,因此對任何字符串的操作如拼接,修改等都將產生一個新的字符串對象,而不是基於原字符串,因此這種持續的 copy 會在一定程度上影響 python 的性能。對字符串的優化也是改善性能的一個重要的方面,特別是在處理文本較多的情況下。字符串的優化主要集中在以下幾個方面:
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、當對字符串可以使用正則表達式或者內置函數來處理的時候,選擇內置函數。如 str.isalpha(),str.isdigit(),str.startswith((‘x’, ‘yz’)),str.endswith((‘x’, ‘yz’))
3、對字符進行格式化比直接串聯讀取要快,因此要使用
- out = "%s%s%s%s" % (head, prologue, query, tail)
- out = "" + head + prologue + query + tail + ""
10、其他優化技巧
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、在循環的時候使用 xrange 而不是 range。使用 xrange 可以節省大量的系統內存,因爲 xrange() 在序列中每次調用只產生一個整數元素。而 range() 將直接返回完整的元素列表,用於循環時會有不必要的開銷。在 python3 中 xrange 不再存在,裏面 range 提供一個可以遍歷任意長度的範圍的 iterator。
3、使用局部變量,避免”global” 關鍵字。python 訪問局部變量會比全局變量要快得多,因 此可以利用這一特性提升性能。
4、if done is not None 比語句 if done != None 更快,讀者可以自行驗證;
5、在耗時較多的循環中,可以把函數的調用改爲內聯的方式;
6、使用級聯比較 “x < y < z” 而不是 “x < y and y < z”;
7、while 1 要比 while True 更快(當然後者的可讀性更好);
8、build in 函數通常較快,add(a,b) 要優於 a+b。
11、定位程序性能瓶頸
對代碼優化的前提是需要了解性能瓶頸在什麼地方,程序運行的主要時間是消耗在哪裏,對於比較複雜的代碼可以藉助一些工具來定位,python 內置了豐富的性能分析工具,如 profile,cProfile 與 hotshot 等。其中 Profiler 是 python 自帶的一組程序,能夠描述程序運行時候的性能,並提供各種統計幫助用戶定位程序的性能瓶頸。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()")
- 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:表示函數調用的次數;
●tottime:表示指定函數的總的運行時間,除掉函數中調用子函數的運行時間;
●percall:(第一個 percall)等於 tottime/ncalls;
●cumtime:表示該函數及其所有子函數的調用運行的時間,即函數開始調用到返回的時間;
●percall:(第二個 percall)即函數運行一次的平均時間,等於 cumtime/ncalls;
●filename:lineno(function):每個函數調用的具體信息;
如果需要將輸出以日誌的形式保存,只需要在調用的時候加入另外一個參數。如 profile.run(“profileTest()”,”testprof”)。
對於 profile 的剖析數據,如果以二進制文件的時候保存結果的時候,可以通過 pstats 模塊進行文本報表分析,它支持多種形式的報表輸出,是文本界面下一個較爲實用的工具。使用非常簡單:
- import pstats
- p = pstats.Stats('testprof')
- p.sort_stats("name").print_stats()
其中 sort_stats() 方法能夠對剖分數據進行排序, 可以接受多個排序字段,如 sort_stats(‘name’, ‘file’) 將首先按照函數名稱進行排序,然後再按照文件名進行排序。常見的排序字段有 calls( 被調用的次數 ),time(函數內部運行時間),cumulative(運行的總時間)等。此外 pstats 也提供了命令行交互工具,執行 python – m pstats 後可以通過 help 瞭解更多使用方式。
對於大型應用程序,如果能夠將性能分析的結果以圖形的方式呈現,將會非常實用和直觀,常見的可視化工具有 Gprof2Dot,visualpytune,KCacheGrind 等。
12、性能分析的基本思路
儘管並非每個你寫的Python程序都需要嚴格的性能分析,但瞭解一下Python的生態系統中很多優秀的在你需要做性能分析的時候可以使用的工具仍然是一件值得去做的事。
分析一個程序的性能,最終都歸結爲回答4個基本的問題:
- 程序運行速度有多快?
- 運行速度瓶頸在哪兒?
- 程序使用了多少內存?
- 內存泄露發生在哪裏?
使用time工具粗糙定時
首先,我們可以使用快速然而粗糙的工具:古老的unix工具time,來爲我們的代碼檢測運行時間。
- $ time python yourprogram.py
- real 0m1.028s
- user 0m0.001s
- sys 0m0.003s
- real - 表示實際的程序運行時間
- user - 表示程序在用戶態的cpu總時間
- sys - 表示在內核態的cpu總時間
通過sys和user時間的求和,你可以直觀的得到系統上沒有其他程序運行時你的程序運行所需要的CPU週期。
若sys和user時間之和遠遠少於real時間,那麼你可以猜測你的程序的主要性能問題很可能與IO等待相關。
使用計時上下文管理器進行細粒度計時
我們的下一個技術涉及訪問細粒度計時信息的直接代碼指令。這是一小段代碼,我發現使用專門的計時測量是非常重要的:
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關鍵字和Timer上下文管理器包裝想要計時的代碼塊。它將會在你的代碼塊開始執行的時候啓動計時器,在你的代碼塊結束的時候停止計時器。
這是一個使用上述代碼片段的例子:
- 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
我經常將這些計時器的輸出記錄到文件中,這樣就可以觀察我的程序的性能如何隨着時間進化。
使用分析器逐行統計時間和執行頻率
Robert Kern有一個稱作line_profiler的不錯的項目,我經常使用它查看我的腳步中每行代碼多快多頻繁的被執行。
想要使用它,你需要通過pip安裝該python包:
- $ pip install line_profiler
一旦安裝完成,你將會使用一個稱做“line_profiler”的新模組和一個“kernprof.py”可執行腳本。
想要使用該工具,首先修改你的源代碼,在想要測量的函數上裝飾@profile裝飾器。不要擔心,你不需要導入任何模組。kernprof.py腳本將會在執行的時候將它自動地注入到你的腳步的運行時。
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)
- kernprof.py -l -v fib.py
- 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]
程序使用了多少內存?
現在我們對計時有了較好的理解,那麼讓我們繼續弄清楚程序使用了多少內存。我們很幸運,Fabian Pedregosa模仿Robert Kern的line_profiler實現了一個不錯的內存分析器。
首先使用pip安裝:
- $ pip install -U memory_profiler
- $ pip install psutil
(這裏建議安裝psutil包,因爲它可以大大改善memory_profiler的性能)。
就像line_profiler,memory_profiler也需要在感興趣的函數上面裝飾@profile裝飾器:
- @profile
- def primes(n):
- ...
- ...
- $ 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會話中輸入以下內容:
- %load_ext memory_profiler
- %load_ext line_profiler
- In [1]: from primes import primes
- In [2]: %mprun -f primes primes(1000)
- In [3]: %lprun -f primes primes(1000)
內存泄漏在哪裏?
cPython解釋器使用引用計數做爲記錄內存使用的主要方法。這意味着每個對象包含一個計數器,當某處對該對象的引用被存儲時計數器增加,當引用被刪除時計數器遞減。當計數器到達零時,cPython解釋器就知道該對象不再被使用,所以刪除對象,釋放佔用的內存。
如果程序中不再被使用的對象的引用一直被佔有,那麼就經常發生內存泄漏。
查找這種“內存泄漏”最快的方式是使用Marius Gedminas編寫的objgraph,這是一個極好的工具。該工具允許你查看內存中對象的數量,定位含有該對象的引用的所有代碼的位置。
一開始,首先安裝objgraph:
pip install objgraph
- import pdb; pdb.set_trace()
最普遍的對象是哪些?
在運行的時候,你可以通過執行下述指令查看程序中前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
哪些對象已經被添加或刪除?
我們也可以查看兩個時間點之間那些對象已經被添加或刪除:
- (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 = [1]
- y = [x, [x], {"a":x}]
- import pdb; pdb.set_trace()
- (pdb) import objgraph
- (pdb) objgraph.show_backref([x], filename="/tmp/backrefs.png")
最下面有紅字的盒子是我們感興趣的對象。我們可以看到,它被符號x引用了一次,被列表y引用了三次。如果是x引起了一個內存泄漏,我們可以使用這個方法,通過跟蹤它的所有引用,來檢查爲什麼它沒有自動的被釋放。
回顧一下,objgraph 使我們可以:
- 顯示佔據python程序內存的頭N個對象
- 顯示一段時間以後哪些對象被刪除活增加了
- 在我們的腳本中顯示某個給定對象的所有引用
努力與精度
在本帖中,我給你顯示了怎樣用幾個工具來分析python程序的性能。通過這些工具與技術的武裝,你可以獲得所有需要的信息,來跟蹤一個python程序中大多數的內存泄漏,以及識別出其速度瓶頸。
對許多其他觀點來說,運行一次性能分析就意味着在努力目標與事實精度之間做出平衡。如果感到困惑,那麼就實現能適應你目前需求的最簡單的解決方案。