-
1 # Bean蠶豆
-
2 # 機器之心ProPython Files 部落格釋出了幾篇主題為「Hunting Performance in Python Code」的系列文章,對提升 Python 程式碼的效能的方法進行了介紹。在其中的每一篇文章中,作者都會介紹幾種可用於 Python 程式碼的工具和分析器,以及它們可以如何幫助你更好地在前端(Python 指令碼)和/或後端(Python 直譯器)中找到瓶頸。
程式碼地址:https://github.com/apatrascu/hunting-python-performance
目錄
一、環境設定
二、記憶體分析
三、CPU 分析——Python 指令碼
四、CPU 分析——Python 直譯器(略)
一、環境設定
設定
在深入到基準測試和效能分析之前,首先我們需要一個合適的環境。這意味著我們需要為這項任務配置我們的機器和作業系統。
我的機器的規格如下:
處理器:Intel(R) Xeon(R) CPU E5-2699 v3 @ 2.30GHz
記憶體:32GB
作業系統:Ubuntu 16.04 LTS
Kernel:4.4.0-75-generic
我們的目標是得到可復現的結果,因此要確保我們的資料不會受到其它後臺程序、作業系統配置或任何其它硬體效能提升技術的影響。
讓我們首先從配置用於效能分析的機器開始。
硬體功能
首先,禁用所有硬體效能功能,也就是說要禁用 Intel Turbo Boost 和 Hyper Threading from BIOS/UEFI。
正如其官方網頁上說的那樣,Turbo Boost 是「一種在處理器核心執行,並可以在低於功耗、電流和溫度規格限制的情況下允許它們以高於額定頻率的速度執行的技術。」此外,Hyper Threading 是「一種可以更高效地利用處理器資源的技術,能使每個核心都能多執行緒執行。」
這都是值得我們花錢購買的好東西。那為什麼要在效能分析/基準測試中禁用它們呢?因為使用這些技術會讓我們無法得到可靠的和可復現的結果。這會讓執行過程發生變化。讓我們看個小例子 primes.py,程式碼故意寫得很糟糕。
import time
import statistics
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]
def benchmark():
results = []
gstart = time.time()
for _ in xrange(5):
start = time.time()
count = len(primes(1000000))
end = time.time()
results.append(end-start)
gend = time.time()
mean = statistics.mean(results)
stdev = statistics.stdev(results)
perc = (stdev * 100)/ mean
print "Benchmark duration: %r seconds" % (gend-gstart)
print "Mean duration: %r seconds" % mean
print "Standard deviation: %r (%r %%)" % (stdev, perc)
benchmark()
這段程式碼可在 GitHub 上檢視:https://github.com/apatrascu/hunting-python-performance/blob/master/01.primes.py。你需要執行以下命令安裝一個依賴包:
pip install statistics
讓我們在一個啟用了 Turbo Boost 和 Hyper Threading 的系統中執行它:
python primes.py
Benchmark duration: 1.0644240379333496 seconds
Mean duration: 0.2128755569458008 seconds
Standard deviation: 0.032928838418120374 (15.468585914964498 %)
現在禁用該系統的睿頻加速(Turbo Boost)和超執行緒(Hyper Threading),然後再次執行這段程式碼:
python primes.py
Benchmark duration: 1.2374498844146729 seconds
Mean duration: 0.12374367713928222 seconds
Standard deviation: 0.000684464852339824 (0.553131172568 %)
看看第一個案例的標準差為 15%。這是一個很大的值!假設我們的最佳化只能帶來 6% 的加速,那我們怎麼能將執行過程中的變化(run to run variation)和你的實現的差異區分開?
相對而言,在第二個例子中,標準差減少到了大約 0.6%,我們的新最佳化方案效果清晰可見。
CPU 節能
禁用所有的 CPU 節能設定,並使用固定的 CPU 頻率。這可以透過在 Linux 功率調節器(power governor)中將 intel_pstate 改成 acpi_cpufreq 而實現。
intel_pstate 驅動使用英特爾核心(Sandy Bridge 或更新)處理器的內部調節器實現了一個縮放驅動。acpi_cpufreq 使用了 ACPI Processor Performance States。
下面讓我們先來檢查一下:
$ cpupower frequency-info
analyzing CPU 0:
driver: intel_pstate
CPUs which run at the same hardware frequency: 0
CPUs which need to have their frequency coordinated by software: 0
maximum transition latency: 0.97 ms.
hardware limits: 1.20 GHz - 3.60 GHz
available cpufreq governors: performance, powersave
current policy: frequency should be within 1.20 GHz and 3.60 GHz.
The governor "powersave" may decide which speed to use
within this range.
current CPU frequency is 1.20 GHz.
boost state support:
Supported: yes
Active: yes
可以看到這裡所使用的調節器被設定成了節能模式,而 CPU 的頻率範圍在 1.20 GHz 到 3.60 GHz 之間。這個設定對日常應用來說是很不錯的,但卻會影響到基準測試的結果。
那麼應該給調節器設定什麼值呢?如果我們瀏覽一下文件,我們可以看到我們可以使用以下設定:
高效能(performance):以最大頻率執行 CPU
節能(powersave):以最小頻率執行 CPU
自定義(userspace):按使用者指定的頻率執行 CPU
按需(ondemand):根據當前負載動態調節頻率。可能跳至最高頻率,空閒時又會降低
保守(conservative):根據當前負載動態調節頻率。相比於按需模式,其頻率調節更加漸進
我們要使用效能調節器(performance governor),並將頻率設定成 CPU 支援的最大頻率。如下所示:
$ cpupower frequency-info
analyzing CPU 0:
driver: acpi-cpufreq
CPUs which run at the same hardware frequency: 0
CPUs which need to have their frequency coordinated by software: 0
maximum transition latency: 10.0 us.
hardware limits: 1.20 GHz - 2.30 GHz
available frequency steps: 2.30 GHz, 2.20 GHz, 2.10 GHz, 2.00 GHz, 1.90 GHz, 1.80 GHz, 1.70 GHz, 1.60 GHz, 1.50 GHz, 1.40 GHz, 1.30 GHz, 1.20 GHz
available cpufreq governors: conservative, ondemand, userspace, powersave, performance
current policy: frequency should be within 2.30 GHz and 2.30 GHz.
The governor "performance" may decide which speed to use
within this range.
current CPU frequency is 2.30 GHz.
cpufreq stats: 2.30 GHz:100.00%, 2.20 GHz:0.00%, 2.10 GHz:0.00%, 2.00 GHz:0.00%, 1.90 GHz:0.00%, 1.80 GHz:0.00%, 1.70 GHz:0.00%, 1.60 GHz:0.00%, 1.50 GHz:0.00%, 1.40 GHz:0.00%, 1.30 GHz:0.00%, 1.20 GHz:0.00% (174)
boost state support:
Supported: no
Active: no
現在你已經使用效能調節器將頻率設定成了固定的 2.3 GHz。這是最大的可設定的值,沒有睿頻加速(Turbo Boost),它可以被用在 Xeon E5-2699 v3 上。
要完成設定,請使用管理員許可權執行以下命令:
cpupower frequency-set -g performance
cpupower frequency-set --min 2300000 --max 2300000
如果你沒有 cpupower,可使用以下命令安裝:
sudo apt-get install linux-tools-common linux-header-`uname -r` -y
功率調節器對 CPU 的工作方式有很大的影響。該調節器的預設設定是自動調節頻率以減少功耗。我們不想要這樣的設定,所以從 GRUB 中禁用它。只需要編輯 /boot/grub/grub.cfg(但是如果你在 kernel 升級上很小心,那麼這將會消失)或在 /etc/grub.d/40_custom 中建立一個新的 kernel 入口。我們的 boot 行中必須包含這個 flag: intel_pstate=disable,如下所示:
linux /boot/vmlinuz-4.4.0-78-generic.efi.signed root=UUID=86097ec1-3fa4-4d00-97c7-3bf91787be83 ro intel_pstate=disable quiet splash $vt_handoff
ASLR(地址空間配置隨機發生器)
這個設定是有爭議的,參見 Victor Stinner 的部落格:https://haypo.github.io/journey-to-stable-benchmark-average.html。當我首次建議在基準測試時禁用 ASLR 時,那是為了進一步提升對那時在 CPython 中存在的 Profile Guided Optimizations 的支援。
我為什麼要說這個呢?因為在上面給出的特定硬體上,禁用 ASLR 可以將執行之間的標準差降低至 0.4%。
另一方面,根據在我的個人計算機(Intel Core i7 4710MQ)上的測試,禁用 ASLR 會導致 Victor 所提到的同樣的問題。在更小的 CPU(比如 Intel Atom)上的測試會帶來甚至更大的執行間標準差。
因為這似乎並不是普遍適用的真理,而且很大程度上依賴於硬體/軟體配置,所以對於這個設定,我在啟用後測量一次,再禁用後測量一次,之後再進行比較。
在我的機器上,我透過在 /etc/sysctl.conf. 中加入以下命令禁用了 ASLR。使用 sudo sysctl -p 進行應用。
kernel.randomize_va_space = 0
如果你想在執行時禁用它:
sudo bash -c "echo 0 >| /proc/sys/kernel/randomize_va_space"
如果你想重新啟用:
sudo bash -c "echo 2 >| /proc/sys/kernel/randomize_va_space"
二、記憶體分析
在這一節,我將介紹一些有助於我們解決 Python 中(尤其是使用 PyPy 時)的記憶體消耗難題的工具。
我們為什麼要關心這個問題?為什麼我們不僅僅就關心效能?這些問題的答案相當複雜,但我會總結出來。
PyPy 是一個可選的 Python 直譯器,其相對於 CPython 有一些巨大的優勢:速度(透過其 Just in Time 編譯器)、相容性(幾乎可以替代 CPython)和併發性(使用 stackless 和 greenlets)。
PyPy 的一個缺點是因為其 JIT 和垃圾一樣的回收站實現,它通常會使用比 CPython 更多的記憶體。但是在某些案例中,其的記憶體消耗會比 CPython 少。
下面我們來看看你可以如何測量你的應用使用了多少記憶體。
診斷記憶體使用
memory_profiler
memory_profiler 是一個可用來測量直譯器執行一個負載時的記憶體用量的庫。你可以透過 pip 安裝它:
pip install memory_profiler
另外還要安裝 psutil 依賴包:
pip install psutil
這個工具的優點是它會在一個 Python 指令碼中一行行地顯示記憶體消耗。這可以讓我們找到指令碼中可以被我們重寫的位置。但這種分析有一個缺點。你的程式碼的執行速度比一般指令碼慢 10 到 20 倍。
怎麼使用它?你只需要在你需要測量的函式上直接加上 @profile() 即可。
讓我們看看實際怎麼操作!我們將使用之前用過的素材指令碼作為模型,但做了一點修改,移除了統計部分。程式碼也可在 GitHub 檢視:https://github.com/apatrascu/hunting-python-performance/blob/master/02.primes-v1.py
from memory_profiler import profile
@profile(precision=6)
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]
len(primes(100000))
開始測量時,使用以下 PyPy 命令:
pypy -m memory_profiler 02.primes-v3.py
或者直接在指令碼中匯入 memory_profiler:
pypy -m memory_profiler 02.primes-v3.py
在執行完這行程式碼之後,我們可以看到 PyPy 得到這樣的結果:
Line # Mem usage Increment Line Contents
================================================
54 35.312500 MiB 0.000000 MiB @profile(precision=6)
55 def primes(n):
56 35.351562 MiB 0.039062 MiB if n == 2:
57 return [2]
58 35.355469 MiB 0.003906 MiB elif n < 2:
59 return []
60 35.355469 MiB 0.000000 MiB s = []
61 59.515625 MiB 24.160156 MiB for i in range(3, n+1):
62 59.515625 MiB 0.000000 MiB if i % 2 != 0:
63 59.515625 MiB 0.000000 MiB s.append(i)
64 59.546875 MiB 0.031250 MiB mroot = n ** 0.5
65 59.550781 MiB 0.003906 MiB half = (n + 1) / 2 - 1
66 59.550781 MiB 0.000000 MiB i = 0
67 59.550781 MiB 0.000000 MiB m = 3
68 59.554688 MiB 0.003906 MiB while m <= mroot:
69 59.554688 MiB 0.000000 MiB if s[i]:
70 59.554688 MiB 0.000000 MiB j = (m * m - 3) / 2
71 59.554688 MiB 0.000000 MiB s[j] = 0
72 59.554688 MiB 0.000000 MiB while j < half:
73 59.554688 MiB 0.000000 MiB s[j] = 0
74 59.554688 MiB 0.000000 MiB j += m
75 59.554688 MiB 0.000000 MiB i = i + 1
76 59.554688 MiB 0.000000 MiB m = 2 * i + 3
77 59.554688 MiB 0.000000 MiB l = [2]
78 59.679688 MiB 0.125000 MiB for x in s:
79 59.679688 MiB 0.000000 MiB if x:
80 59.679688 MiB 0.000000 MiB l.append(x)
81 59.683594 MiB 0.003906 MiB return l
我們可以看到這個指令碼使用了 24.371094 MiB 的 RAM。讓我們簡單分析一下。我們看到其中大多數都用在了數值陣列的構建中。它排除了偶數數值,保留了所有其它數值。
我們可以透過呼叫 range 函式而對其進行一點改進,其使用一個增量引數。在這個案例中,該指令碼看起來像是這樣:
from memory_profiler import profile
@profile(precision=6)
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
l = [2]
for x in s:
if x:
l.append(x)
return l
len(primes(100000))
如果我們再次測量,我們可以得到以下結果:
Line # Mem usage Increment Line Contents
================================================
27 35.343750 MiB 0.000000 MiB @profile(precision=6)
28 def primes(n):
29 35.382812 MiB 0.039062 MiB if n == 2:
30 return [2]
31 35.382812 MiB 0.000000 MiB elif n < 2:
32 return []
33 35.386719 MiB 0.003906 MiB s = range(3, n + 1, 2)
34 35.417969 MiB 0.031250 MiB mroot = n ** 0.5
35 35.417969 MiB 0.000000 MiB half = (n + 1) / 2 - 1
36 35.417969 MiB 0.000000 MiB i = 0
37 35.421875 MiB 0.003906 MiB m = 3
38 58.019531 MiB 22.597656 MiB while m <= mroot:
39 58.019531 MiB 0.000000 MiB if s[i]:
40 58.019531 MiB 0.000000 MiB j = (m * m - 3) / 2
41 58.019531 MiB 0.000000 MiB s[j] = 0
42 58.019531 MiB 0.000000 MiB while j < half:
43 58.019531 MiB 0.000000 MiB s[j] = 0
44 58.019531 MiB 0.000000 MiB j += m
45 58.019531 MiB 0.000000 MiB i = i + 1
46 58.019531 MiB 0.000000 MiB m = 2 * i + 3
47 58.019531 MiB 0.000000 MiB l = [2]
48 58.089844 MiB 0.070312 MiB for x in s:
49 58.089844 MiB 0.000000 MiB if x:
50 58.089844 MiB 0.000000 MiB l.append(x)
51 58.093750 MiB 0.003906 MiB return l
很好,現在我們的記憶體消耗下降到了 22.75 MiB。使用列表解析(list comprehension),我們還可以將消耗再降低一點。
from memory_profiler import profile
@profile(precision=6)
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]
len(primes(100000))
再次測量:
Line # Mem usage Increment Line Contents
================================================
4 35.425781 MiB 0.000000 MiB @profile(precision=6)
5 def primes(n):
6 35.464844 MiB 0.039062 MiB if n == 2:
7 return [2]
8 35.464844 MiB 0.000000 MiB elif n < 2:
9 return []
10 35.464844 MiB 0.000000 MiB s = range(3, n + 1, 2)
11 35.500000 MiB 0.035156 MiB mroot = n ** 0.5
12 35.500000 MiB 0.000000 MiB half = (n + 1) / 2 - 1
13 35.500000 MiB 0.000000 MiB i = 0
14 35.500000 MiB 0.000000 MiB m = 3
15 57.683594 MiB 22.183594 MiB while m <= mroot:
16 57.683594 MiB 0.000000 MiB if s[i]:
17 57.683594 MiB 0.000000 MiB j = (m * m - 3) / 2
18 57.683594 MiB 0.000000 MiB s[j] = 0
19 57.683594 MiB 0.000000 MiB while j < half:
20 57.683594 MiB 0.000000 MiB s[j] = 0
21 57.683594 MiB 0.000000 MiB j += m
22 57.683594 MiB 0.000000 MiB i = i + 1
23 57.683594 MiB 0.000000 MiB m = 2 * i + 3
24 57.847656 MiB 0.164062 MiB return [2] + [x for x in s if x]
我們最後的指令碼僅消耗 22.421875 MiB。相比於第一個版本,差不多下降了 10%。
三、CPU 分析——Python 指令碼
在這一節,我將介紹一些有助於我們解決 Python 中的分析 CPU 使用的難題的工具。
CPU 效能分析(profiling)的意思是透過分析 CPU 執行程式碼的方式來分析這些程式碼的效能。也就是說要找到我們程式碼中的熱點(hot spot),然後看我們可以怎麼處理它們。
接下來我們會看看你可以如何追蹤你的 Python 指令碼的 CPU 使用。我們將關注以下分析器(profiler):
* cProfile
* line_profiler
* pprofile
* vprof
測量 CPU 使用
import time
def primes(n):
if n == 2:
return [2]
elif n < 2:
return []
s = []
for i in range(3, n+1):
if i % 2 != 0:
s.append(i)
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
l = [2]
for x in s:
if x:
l.append(x)
return l
def benchmark():
start = time.time()
for _ in xrange(40):
count = len(primes(1000000))
end = time.time()
print "Benchmark duration: %r seconds" % (end-start)
benchmark()
這一節我將使用與前一節基本一樣的指令碼,你也可以在 GitHub 上檢視:https://gist.github.com/apatrascu/8524679175de08a54a95e22001a31d3b
另外,記住在 PyPy2 上,你需要使用一個支援它的 pip 版本:
pypy -m ensure pip
其它東西可以透過以下指令安裝:
pypy -m pip install
cProfile
在 CPU 效能分析上最常用的一個工具是 cProfile,主要是因為它內置於 CPython2 和 PyPy2 中。這是一個確定性的分析器,也就是說它會在執行我們的負載時收集一系列統計資料,比如程式碼各個部分的執行次數或執行時間。此外,相比於其它內建的分析器(profile 或 hotshot),cProfile 對系統的開銷更少。
當使用 CPython2 時,其使用方法是相當簡單的:
python -m cProfile 03.primes-v1.py
如果你使用的是 PyPy2:
pypy -m cProfile 03.primes-v1.py
其輸出如下:
Benchmark duration: 30.11158514022827 seconds
23139965 function calls in 30.112 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 30.112 30.112 03.primes.py:1(<module>)
40 19.760 0.494 29.896 0.747 03.primes.py:3(primes)
1 0.216 0.216 30.112 30.112 03.primes.py:31(benchmark)
40 0.000 0.000 0.000 0.000 {len}
23139840 6.683 0.000 6.683 0.000 {method "append" of "list" objects}
1 0.000 0.000 0.000 0.000 {method "disable" of "_lsprof.Profiler" objects}
40 3.453 0.086 3.453 0.086 {range}
2 0.000 0.000 0.000 0.000 {time.time}
即使是這樣的文字輸出,我們也可以直接看到我們指令碼的大多數時間都在呼叫 list.append 方法。
如果我們使用 gprof2dot,我們可以用圖形化的方式來檢視 cProfile 的輸出。要使用這個工具,我們首先必須安裝 graphviz。在 Ubuntu 上,可以使用以下命令:
apt-get install graphviz
pip install gprof2dot
再次執行我們的指令碼:
python -m cProfile -o output.pstats 03.primes-v1.py
gprof2dot -f pstats output.pstats | dot -Tpng -o output.png
然後我們會得到下面的 output.png 檔案:
這樣看起來就輕鬆多了。讓我們仔細看看它輸出了什麼。你可以看到來自你的指令碼的一個函式呼叫圖(callgraph)。在每個方框中,你可以一行一行地看到:
第一行:Python 檔名、行數和方法名
第二行:這個方框所用的時間佔全局時間的比例
第三行:括號中是該方法本身所用時間佔全局時間的比例
第四行:呼叫次數
比如說,在從上到下第三個紅色框中,方法 primes 佔用了 98.28% 的時間,65.44% 的時間是在該方法之中做什麼事情,它被呼叫了 40 次。剩下的時間被用在了 Python 的 list.append(22.33%)和 range(11.51%)方法中。
這是一個簡單的指令碼,所以我們只需要重寫我們的指令碼,讓它不用使用那麼多 append,結果如下:
import time
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]
def benchmark():
start = time.time()
for _ in xrange(40):
count = len(primes(1000000))
end = time.time()
print "Benchmark duration: %r seconds" % (end-start)
benchmark()
如果我們在此之前和之後使用 CPython2 測量指令碼的時間:
python 03.primes-v1.py
Benchmark duration: 15.768115043640137 seconds
python 03.primes-v2.py
Benchmark duration: 6.56312108039856 seconds
用 PyPy2 測量:
pypy 03.primes-v1.py
Benchmark duration: 1.4009230136871338 seconds
pypy 03.primes-v2.py
Benchmark duration: 0.4542720317840576 seconds
我們在 CPython2 上得到了 2.4 倍的提升,在 PyPy2 上得到了 3.1 倍的提升。很不錯,其 cProfile 呼叫圖為:
你也可以以程式的方式檢視 cProfile:
import cProfile
pr = cProfile.Profile()
pr.enable()
function_to_measure()
pr.disable()
pr.print_stats(sort="time")
這在一些場景中很有用,比如多程序效能測量。更多詳情請參閱:https://docs.python.org/2/library/profile.html#module-cProfile
line_profiler
這個分析器可以提供逐行水平的負載資訊。這是透過 C 語言用 Cython 實現的,與 cProfile 相比計算開銷更少。
其原始碼可在 GitHub 上獲取:https://github.com/rkern/line_profiler,PyPI 頁面為:https://pypi.python.org/pypi/line_profiler/。和 cProfile 相比,它有相當大的開銷,需要多 12 倍的時間才能得到一個分析結果。
要使用這個工具,你首先需要透過 pip 新增:pip install pip install Cython ipython==5.4.1 line_profiler(CPython2)。這個分析器的一個主要缺點是不支援 PyPy。
就像在使用 memory_profiler 時一樣,你需要在你想分析的函式上加上一個裝飾。在我們的例子中,你需要在 03.primes-v1.py 中的 primes 函式的定義前加上 @profile。然後像這樣呼叫:
kernprof -l 03.primes-v1.py
python -m line_profiler 03.primes-v1.py.lprof
你會得到一個這樣的輸出:
Timer unit: 1e-06 s
Total time: 181.595 s
File: 03.primes-v1.py
Function: primes at line 3
Line # Hits Time Per Hit % Time Line Contents
==============================================================
3 @profile
4 def primes(n):
5 40 107 2.7 0.0 if n == 2:
6 return [2]
7 40 49 1.2 0.0 elif n < 2:
8 return []
9 40 44 1.1 0.0 s = []
10 39999960 34410114 0.9 18.9 for i in range(3, n+1):
11 39999920 29570173 0.7 16.3 if i % 2 != 0:
12 19999960 14976433 0.7 8.2 s.append(i)
13 40 329 8.2 0.0 mroot = n ** 0.5
14 40 82 2.0 0.0 half = (n + 1) / 2 - 1
15 40 46 1.1 0.0 i = 0
16 40 30 0.8 0.0 m = 3
17 20000 17305 0.9 0.0 while m <= mroot:
18 19960 16418 0.8 0.0 if s[i]:
19 6680 6798 1.0 0.0 j = (m * m - 3) / 2
20 6680 6646 1.0 0.0 s[j] = 0
21 32449400 22509523 0.7 12.4 while j < half:
22 32442720 26671867 0.8 14.7 s[j] = 0
23 32442720 22913591 0.7 12.6 j += m
24 19960 15078 0.8 0.0 i = i + 1
25 19960 16170 0.8 0.0 m = 2 * i + 3
26 40 87 2.2 0.0 l = [2]
27 20000000 14292643 0.7 7.9 for x in s:
28 19999960 13753547 0.7 7.6 if x:
29 3139880 2417421 0.8 1.3 l.append(x)
30 40 33 0.8 0.0 return l
我們可以看到兩個迴圈在反覆呼叫 list.append,佔用了指令碼的大部分時間。
pprofile
地址:http://github.com/vpelletier/pprofile
據作者介紹,pprofile 是一個「行粒度的、可感知執行緒的確定性和統計性純 Python 分析器」。
它的靈感來源於 line_profiler,修復了大量缺陷,但因為其完全是用 Python 寫的,所以也可以透過 PyPy 使用。和 cProfile 相比,使用 CPython 時分析的時間會多 28 倍,使用 PyPy 時的分析時間會長 10 倍,但具有粒度更大的細節水平。
而且還支援 PyPy 了!除此之外,它還支援執行緒分析,這在很多情況下都很有用。
要使用這個工具,你首先需要透過 pip 安裝:pip install pprofile(CPython2)/ pypy -m pip install pprofile(PyPy),然後像這樣呼叫:
pprofile 03.primes-v1.py
其輸出和前面工具的輸出不同,如下:
Benchmark duration: 886.8774709701538 seconds
Command line: ["03.primes-v1.py"]
Total duration: 886.878s
File: 03.primes-v1.py
File duration: 886.878s (100.00%)
Line #| Hits| Time| Time per hit| %|Source code
------+----------+-------------+-------------+-------+-----------
1| 2| 7.10487e-05| 3.55244e-05| 0.00%|import time
2| 0| 0| 0| 0.00%|
3| 0| 0| 0| 0.00%|
4| 41| 0.00029397| 7.17e-06| 0.00%|def primes(n):
5| 40| 0.000231266| 5.78165e-06| 0.00%| if n == 2:
6| 0| 0| 0| 0.00%| return [2]
7| 40| 0.000178337| 4.45843e-06| 0.00%| elif n < 2:
8| 0| 0| 0| 0.00%| return []
9| 40| 0.000188112| 4.70281e-06| 0.00%| s = []
10| 39999960| 159.268| 3.98171e-06| 17.96%| for i in range(3, n+1):
11| 39999920| 152.924| 3.82312e-06| 17.24%| if i % 2 != 0:
12| 19999960| 76.2135| 3.81068e-06| 8.59%| s.append(i)
13| 40| 0.00147367| 3.68416e-05| 0.00%| mroot = n ** 0.5
14| 40| 0.000319004| 7.9751e-06| 0.00%| half = (n + 1) / 2 - 1
15| 40| 0.000220776| 5.51939e-06| 0.00%| i = 0
16| 40| 0.000243902| 6.09756e-06| 0.00%| m = 3
17| 20000| 0.0777466| 3.88733e-06| 0.01%| while m <= mroot:
18| 19960| 0.0774016| 3.87784e-06| 0.01%| if s[i]:
19| 6680| 0.0278566| 4.17015e-06| 0.00%| j = (m * m - 3) / 2
20| 6680| 0.0275929| 4.13067e-06| 0.00%| s[j] = 0
21| 32449400| 114.858| 3.5396e-06| 12.95%| while j < half:
22| 32442720| 120.841| 3.72475e-06| 13.63%| s[j] = 0
23| 32442720| 114.432| 3.5272e-06| 12.90%| j += m
24| 19960| 0.0749919| 3.75711e-06| 0.01%| i = i + 1
25| 19960| 0.0765574| 3.83554e-06| 0.01%| m = 2 * i + 3
26| 40| 0.000222206| 5.55515e-06| 0.00%| l = [2]
27| 20000000| 68.8031| 3.44016e-06| 7.76%| for x in s:
28| 19999960| 67.9391| 3.39696e-06| 7.66%| if x:
29| 3139880| 10.9989| 3.50295e-06| 1.24%| l.append(x)
30| 40| 0.000155687| 3.89218e-06| 0.00%| return l
31| 0| 0| 0| 0.00%|
32| 0| 0| 0| 0.00%|
33| 2| 8.10623e-06| 4.05312e-06| 0.00%|def benchmark():
34| 1| 5.00679e-06| 5.00679e-06| 0.00%| start = time.time()
35| 41| 0.00101089| 2.4656e-05| 0.00%| for _ in xrange(40):
36| 40| 0.232263| 0.00580657| 0.03%| count = len(primes(1000000))
(call)| 40| 886.644| 22.1661| 99.97%|# 03.primes-v1.py:4 primes
37| 1| 5.96046e-06| 5.96046e-06| 0.00%| end = time.time()
38| 1| 0.000678062| 0.000678062| 0.00%| print "Benchmark duration: %r seconds" % (end-start)
39| 0| 0| 0| 0.00%|
40| 0| 0| 0| 0.00%|
41| 1| 5.79357e-05| 5.79357e-05| 0.00%|benchmark()
(call)| 1| 886.878| 886.878|100.00%|# 03.primes-v1.py:33 benchmark
我們現在可以看到更詳細的細節。讓我們稍微研究一下這個輸出。這是這個指令碼的整個輸出,每一行你可以看到呼叫的次數、執行它所用的時間(秒)、每次呼叫的時間和佔全局時間的比例。此外,pprofile 還為我們的輸出增加了額外的行(比如 44 和 50 行,行前面寫著 (call)),這是累積指標。
同樣,我們可以看到有兩個迴圈在反覆呼叫 list.append,佔用了指令碼的大部分時間。
vprof
地址:https://github.com/nvdv/vprof
vprof 是一個 Python 分析器,為各種 Python 程式特點提供了豐富的互動式視覺化,比如執行時間和記憶體使用。這是一個圖形化工具,基於 Node.JS,可在網頁上展示結果。
使用這個工具,你可以針對相關 Python 指令碼檢視下面的一項或多項內容:
CPU flame graph
程式碼分析(code profiling)
記憶體圖(memory graph)
程式碼熱圖(code heatmap)
要使用這個工具,你首先需要透過 pip 安裝:pip install vprof(CPython2)/ pypy -m pip install vprof(PyPy)。
在 CPython2 上,要顯示程式碼熱圖(下面的第一行呼叫)和程式碼分析(下面的第二行呼叫):
vprof -c h 03.primes-v1.py
vprof -c p 03.primes-v1.py
在 PyPy 上,要顯示程式碼熱圖(下面的第一行呼叫)和程式碼分析(下面的第二行呼叫):
pypy -m vprof -c h 03.primes-v1.py
pypy -m vprof -c p 03.primes-v1.py
在上面的兩個例子中,你都會看到如下的程式碼熱圖:
以及如下的程式碼分析:
同樣,我們可以看到有兩個迴圈在反覆呼叫 list.append,佔用了指令碼的大部分時間。
回覆列表
如何提高python程式碼的效能,我的看法是要要關注程式碼本身,這裡我覺得有幾點是我們寫程式碼的時候要注意的。
利用assert語句來發現問題,我們不能確保我們的邏輯是完全正確的,透過斷言來發現問題,提高程式碼的執行效率斷言很多語言都存在,它主要為排程程式服務,能夠快速方便地檢查程式的異常或者發現不恰當的輸入等,可防止意想不到的情況出現。防止程式崩或者死迴圈這類差體驗的程式碼出現。
資料交換值的時候不推薦使用中間變數在其它語言中,C語言尤其明顯,temp是變數在交換值是必須需要用的一個變數,不然值就交換不了。在Python中也可以這樣做,但是不推薦,利用a, b = b, a這種更加pythonic的程式碼方式,可以讓我們獲取到量多好的效能,從時間上來看Pyhonic的方式時間會更短,常規的變數替換方式的時間會更長。
主要是因為Python表示式計算順序是從左到右,但遇到表達賦值的時候表示式右邊的操作先於左加的操作,因此計算順序就變成b, a->a, b
做資料計算的時候儘量轉換為浮點型別後再做除法python在最初的設計的時候借鑑C語言的一些規則,但是C語言有個特別的地方是-變數在使用前會預先申明型別,當型別不符時,編譯器會盡可能進行強制轉換,否則編譯報錯。但是Python作為 一門高階語言,我們就不能提前申明返回的結果是浮點型別,當除法運算中兩個運算元是整數的時候,其返回值為整數,這和實際的結果會有質的區別。因此,我們在做計算的時候,儘量地轉換資料型別。