本文是《Mastering Python High Performance》的讀書筆記。python
做者(出版社)開源的代碼地址:c++
《Mastering Python High Performance》一書大體分爲兩部分,第一部分講了profile
的方法論,介紹了cProfile
和line_profile
的使用。第二部分介紹了一些提升性能的方法。這篇文章只講第二部分的第四章提到的一些方法。編程
我對本文中出現的一些例子作了必定修改。sass
對於一些耗時、輸入參數大體固定的函數,若是你可以保證必定的輸出必定能夠獲得相同的結果,能夠把結果保存起來,以後調用的時候就無需作重複計算。書中給出了一個修飾器:bash
class Memoized:
def __init__(self, fn):
self.fn = fn
self.results = {}
def __call__(self, *args, **kwargs):
key = ''.join([str(arg) for arg in args] + ["{}:{}".format(k, v) for k, v in kwargs.items()])
try:
return self.results[key]
except KeyError:
self.results[key] = self.fn(*args)
return self.results[key]
複製代碼
利用這個能夠更快速地計算斐波那契數列:微信
@Memoized
def fib(n):
if n == 0: return 0
if n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)
複製代碼
測試一下性能:計算fib(40)
。使用了Memoized
修飾器地時候花了0.0005
秒,不用的時候個人垃圾 macbook air 跑了106.4099
秒,可見性能差距之大。併發
第二個(優化過的)factor
參數會在函數建立時就被進行計算。app
import math
def degree_sin(deg):
return math.sin(deg * math.pi / 180.0) * math.cos(deg * math.pi / 180.0)
def degree_sin_opt(deg, factor=math.pi / 180.0, sin=math.sin, cos=math.cos):
return sin(deg * factor) * cos(deg * factor)
複製代碼
每一個函數執行1000次而後計算平均值,發現第二種地性能有了明顯提高。緣由在於第二種參數只須要在函數第一次建立的時候計算一次異步
可是這種方法若是不作很好地文檔說明的話,有可能會帶來問題。除了可以代理顯著的性能提高,不建議使用。async
用列表生成器比 for 循環性能要高。
# 類別生成器
multiples_of_two = [x for x in range(100) if x % 2 == 0]
# for 循環
multiples_of_two = []
for x in range(100):
if x % 2 == 0:
multiples_of_two.append(x)
複製代碼
要搞清楚問什麼,須要看看底層的 bytecodes,這裏使用到了dis
模塊。同時,timeit.timeit()
方法默認執行 code 1000000次,你也能夠經過 number
指定運行次數。
import dis
import timeit
programs = dict(
comprehension_code=""" [x for x in range(100) if x % 2 == 0] """,
forloop_code=""" multiples_of_two = [] for x in range(100): if x % 2 == 0: multiples_of_two.append(x) """
)
for program, code in programs.items():
dis.dis(code)
print("{} spent {} seconds".format(program, timeit.timeit(stmt=code)))
print()
複製代碼
你不須要徹底看懂每一行 bytecode,簡單來講,list comprehension有更少的 bytecode,同時用了優化過的LIST_APPEND
指令,而 for 循環中用的是LOAD_ATTR, LOAD_NAME, CALL_FUNCTION
這三條指令。
2 0 LOAD_CONST 0 (<code object <listcomp> at 0x102e5f420, file "<dis>", line 2>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (range)
8 LOAD_CONST 2 (100)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x102e5f420, file "<dis>", line 2>:
2 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 20 (to 26)
6 STORE_FAST 1 (x)
8 LOAD_FAST 1 (x)
10 LOAD_CONST 0 (2)
12 BINARY_MODULO
14 LOAD_CONST 1 (0)
16 COMPARE_OP 2 (==)
18 POP_JUMP_IF_FALSE 4
20 LOAD_FAST 1 (x)
22 LIST_APPEND 2
24 JUMP_ABSOLUTE 4
>> 26 RETURN_VALUE
comprehension_code spent 10.200395356 seconds
2 0 BUILD_LIST 0
2 STORE_NAME 0 (multiples_of_two)
3 4 SETUP_LOOP 38 (to 44)
6 LOAD_NAME 1 (range)
8 LOAD_CONST 0 (100)
10 CALL_FUNCTION 1
12 GET_ITER
>> 14 FOR_ITER 26 (to 42)
16 STORE_NAME 2 (x)
4 18 LOAD_NAME 2 (x)
20 LOAD_CONST 1 (2)
22 BINARY_MODULO
24 LOAD_CONST 2 (0)
26 COMPARE_OP 2 (==)
28 POP_JUMP_IF_FALSE 14
5 30 LOAD_NAME 0 (multiples_of_two)
32 LOAD_METHOD 3 (append)
34 LOAD_NAME 2 (x)
36 CALL_METHOD 1
38 POP_TOP
40 JUMP_ABSOLUTE 14
>> 42 POP_BLOCK
>> 44 LOAD_CONST 3 (None)
46 RETURN_VALUE
forloop_code spent 15.124301844 seconds
複製代碼
若是你不須要一次性所有生成全部的元素,只是想一個一個遍歷,List comprehension能夠進一步優化成 generators:
def gene_multiples_of_two(most):
x = 0
while x <= most:
if x % 2 == 0:
yield x
x += 1
複製代碼
注意只有cpython
能夠用ctypes
擴展,由於cpython
是用 c 寫的,可是其餘的好比Jython
、PyPy
不行。你能夠引入已經編譯過的 c code。好比 Windows 的kernel32.dll
,msvcrt.dll
和Linux 的libc.so.6
。
如今咱們要找出一百萬之內的全部質數,純python的寫法以下(這裏不用關心質數判斷函數的原理,若是你想了解,點這裏):
from math import sqrt
from itertools import count, islice
def is_prime(n):
return n > 1 and all(n % i for i in islice(count(2), int(sqrt(n) - 1)))
[x for x in range(1000000) if is_prime(x)]
複製代碼
一共花了14秒。
用 c 寫一個 Shared library:check_prime.c
:
#include <stdio.h>
#include <math.h>
int check_prime(int a)
{
int c;
for ( c = 2 ; c <= sqrt(a) ; c++ ) {
if ( a%c == 0 )
return 0;
}
return 1;
}
複製代碼
gcc -shared -o check_prime.so -fPIC check_prime.c
複製代碼
在 Python 代碼中引入:
check_primes_types = ctypes.CDLL('./check_prime.so').check_prime
[x for x in range(1000000) if check_primes_types(x)]
複製代碼
如今耗時2s,性能提高了7倍。
cpython 對於字符串的處理和其餘語言不太同樣,若是有兩個變量a
和b
,值都是hello world
,那麼在內存中,他們實際上指向的是同一個東西,若是修改 b 的值,會將 b 指向另一個字符串:
若是再把 a
的值修改一下,內存中的hello world
將會被 gc 掉。一開始 Python 的這種方式看起來很奇怪,可是這不是沒有道理的:
That being said, immutable objects are not all that bad. They are actually good for performance if used right, since they can be used as dictionary keys, for instance, or even shared between different variable bindings (since the same block of memory is used every time you reference the same string). This means that the string hey there will be the same exact object every time you use that string, no matter what variable it is stored in (like we saw earlier).
字符串能夠被用於字典的 keys。不一樣的變量可能會指向同一個字符串,因此這些內存就能夠共享,從而在必定程度上節省了內存。
這樣的設計對於咱們開發者而言有什麼影響呢?看下面這個例子。這個例子內存使用是有問題的,你看出來了嗎?每次循環,都會建立一個新的字符串。
full_doc = ""
words = [str(x) for x in range(1000000)]
for word in words:
full_doc += word
複製代碼
用列表生成器會更加高效:
full_doc = "".join(world_list)
複製代碼
相似的還有:
document = title + introduction + main_piece + conclusion
複製代碼
會建立沒必要要的中間變量,用下面這種方法會好一點:
document = "%s%s%s%s" % (title, introduction, main_piece, conclusion)
複製代碼
初學者每每對併發
和並行
兩個概念搞不清,認爲只有並行
才能併發。這是一個很大的話題,同時我以爲《Mastering Python High Performance》這本書這部分講得並沒什麼特別好的地方,能夠看一下我前面寫的幾篇文章:
map(operator.add, list1, list2
會比map(lambda x, y: x+y, list1, list2)
高效。collections.deque
,當須要頻繁使用pop
、insert
的時候,deque
會比list
更加高效,由於deque
有 O(1)的 pop(0)
、pop(-1)
和 append
性能。l.sort(key=lambda a: a[1])
複製代碼
要比
l.sort(cmp=lambda a,b: cmp(a[1], b[1]))
複製代碼
更加高效。
若是你像我同樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注個人微信公衆號: