# Profiling
> 原文: [http://docs.cython.org/en/latest/src/tutorial/profiling_tutorial.html](http://docs.cython.org/en/latest/src/tutorial/profiling_tutorial.html)
這部分描述了 Cython 的性能分析能力。如果您熟悉純 Python 代碼的分析,則只能閱讀第一部分( [Cython Profiling Basics](#profiling-basics))。如果您不熟悉 Python 分析,您還應該閱讀教程( [分析教程](#profiling-tutorial) ),它將逐步引導您完成一個完整的示例。
## Cython Profiling Basics
Cython 中的分析由編譯器指令控制。它可以通過 Cython 裝飾器設置為整個文件或基于每個功能。
### 啟用完整源文件的分析
通過全局指令將完整源文件的分析啟用到文件頂部的 Cython 編譯器:
```py
# cython: profile=True
```
請注意,分析會給每個函數調用帶來輕微的開銷,因此會使程序變慢(或者很多,如果你經常調用一些小函數)。
啟用后,從 cProfile 模塊調用時,您的 Cython 代碼將像 Python 代碼一樣運行。這意味著您可以使用與僅 Python 代碼相同的工具,將您的 Cython 代碼與 Python 代碼一起分析。
### 禁用分析功能
如果您的分析因為某些小功能的調用開銷而變得混亂,而您希望在您的配置文件中看不到這些功能 - 無論是因為您計劃內聯它們還是因為您確定不能讓它們更快 - 你可以使用一個特殊的裝飾器來禁用一個函數的分析(無論它是否全局啟用):
```py
cimport cython
@cython.profile(False)
def my_often_called_function():
pass
```
### 啟用行跟蹤
要獲得更詳細的跟蹤信息(對于可以使用它的工具),您可以啟用行跟蹤:
```py
# cython: linetrace=True
```
這也將啟用分析支持,因此不需要上面的`profile=True`選項。例如,覆蓋率分析需要線跟蹤。
請注意,即使通過編譯器指令啟用了行跟蹤,默認情況下也不會使用它。由于運行時減速可能很大,因此必須由 C 編譯器通過設置 C 宏定義`CYTHON_TRACE=1`進行編譯。要在跟蹤中包含 nogil 函數,請設置`CYTHON_TRACE_NOGIL=1`(表示`CYTHON_TRACE=1`)。可以在`setup.py`腳本的擴展定義中定義 C 宏,也可以使用以下文件頭注釋設置源文件中的相應 distutils 選項(如果`cythonize()`用于編譯):
```py
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
```
### 啟用覆蓋率分析
從 Cython 0.23 開始,線跟蹤(見上文)也支持使用 [coverage.py](https://coverage.readthedocs.io/) 工具報告覆蓋率報告。要使覆蓋率分析了解 Cython 模塊,還需要在`.coveragerc`文件中啟用 Cython 的 coverage 插件,如下所示:
```py
[run]
plugins = Cython.Coverage
```
使用此插件,您的 Cython 源文件應該在 coverage 報告中正常顯示。
要將覆蓋率報告包含在 Cython 帶注釋的 HTML 文件中,您需要首先運行 coverage.py 工具以生成 XML 結果文件。將此文件傳遞到`cython`命令,如下所示:
```py
$ cython --annotate-coverage coverage.xml package/mymodule.pyx
```
這將重新編譯 Cython 模塊并在其處理的每個 Cython 源文件旁邊生成一個 HTML 輸出文件,其中包含 coverage 報告中包含的行的顏色標記。
## 分析教程
這將是一個完整的教程,從頭到尾,分析 Python 代碼,將其轉換為 Cython 代碼并保持分析直到它足夠快。
作為一個玩具示例,我們想要評估平方倒數的總和,直到某個整數來評估。我們想要使用的關系已經由歐拉在 1735 年證明并被稱為[巴塞爾問題](https://en.wikipedia.org/wiki/Basel_problem)。

用于評估截斷總和的簡單 Python 代碼如下所示:
```py
# calc_pi.py
def recip_square(i):
return 1. / i ** 2
def approx_pi(n=10000000):
val = 0.
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
```
在我的盒子上,這需要大約 4 秒來運行默認 n 的函數。我們選擇 n 越高,的近似值越好。經驗豐富的 Python 程序員已經看到很多地方可以優化這段代碼。但請記住優化的黃金法則:永不優化而不進行分析。讓我重復一遍:**從不**優化而不分析您的代碼。您對代碼的哪一部分花費太多時間的想法是錯誤的。至少,我的總是錯的。所以讓我們編寫一個簡短的腳本來分析我們的代碼:
```py
# profile.py
import pstats, cProfile
import calc_pi
cProfile.runctx("calc_pi.approx_pi()", globals(), locals(), "Profile.prof")
s = pstats.Stats("Profile.prof")
s.strip_dirs().sort_stats("time").print_stats()
```
在我的盒子上運行它給出以下輸出:
```py
Sat Nov 7 17:40:54 2009 Profile.prof
10000004 function calls in 6.211 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 3.243 3.243 6.211 6.211 calc_pi.py:7(approx_pi)
10000000 2.526 0.000 2.526 0.000 calc_pi.py:4(recip_square)
1 0.442 0.442 0.442 0.442 {range}
1 0.000 0.000 6.211 6.211 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
```
它包含代碼在 6.2 CPU 秒內運行的信息。請注意,代碼慢了 2 秒,因為它在 cProfile 模塊中運行。該表包含真正有價值的信息。您可能需要查看 Python [分析文檔](https://docs.python.org/library/profile.html)以獲取詳細信息。這里最重要的列是 totime(在此函數中花費的總時間**而不是**計算由此函數調用的函數)和 cumtime(在此函數中花費的總時間**也計算所謂的函數**通過這個功能)。查看 tottime 列,我們看到大約一半的時間花在 approx_pi 上,另一半花在 recip_square 上。還有半秒鐘在范圍內度過......當然我們應該使用 xrange 進行如此大的迭代。實際上,只需將范圍更改為 xrange 就可以在 5.8 秒內運行代碼。
我們可以在純 Python 版本中進行優化,但由于我們對 Cython 感興趣,讓我們繼續前進并將此模塊帶到 Cython。我們無論如何都會這樣做,以使循環運行得更快。這是我們的第一個 Cython 版本:
```py
# cython: profile=True
# calc_pi.pyx
def recip_square(int i):
return 1. / i ** 2
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
```
注意第一行:我們必須告訴 Cython 應該啟用性能分析。這使得 Cython 代碼稍慢,但如果沒有這個,我們將無法從 cProfile 模塊獲得有意義的輸出。其余代碼大部分都沒有改變,我只輸入了一些可能會加快速度的變量。
我們還需要修改我們的分析腳本以直接導入 Cython 模塊。這是添加 [Pyximport](../userguide/source_files_and_compilation.html#pyximport)模塊導入的完整版本:
```py
# profile.py
import pstats, cProfile
import pyximport
pyximport.install()
import calc_pi
cProfile.runctx("calc_pi.approx_pi()", globals(), locals(), "Profile.prof")
s = pstats.Stats("Profile.prof")
s.strip_dirs().sort_stats("time").print_stats()
```
我們只添加了兩行,其余的完全相同。或者,我們也可以手動將代碼編譯成擴展名;我們根本不需要更改配置文件腳本。該腳本現在輸出以下內容:
```py
Sat Nov 7 18:02:33 2009 Profile.prof
10000004 function calls in 4.406 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 3.305 3.305 4.406 4.406 calc_pi.pyx:7(approx_pi)
10000000 1.101 0.000 1.101 0.000 calc_pi.pyx:4(recip_square)
1 0.000 0.000 4.406 4.406 {calc_pi.approx_pi}
1 0.000 0.000 4.406 4.406 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
```
我們增加了 1.8 秒。不是太寒酸。將輸出與前一個進行比較,我們看到 recip_square 函數變得更快,而 approx_pi 函數沒有發生很大變化。讓我們更專注于 recip_square 函數。首先請注意,不要從我們模塊之外的代碼調用此函數;所以將它變成 cdef 以減少呼叫開銷是明智的。我們也應該擺脫冪運算符:它變成了 Cython 的 pow(i,2)函數調用,但我們可以改為編寫 i * i,這可能更快。整個功能也是內聯的良好候選者。讓我們看看這些想法的必要變化:
```py
# cython: profile=True
# calc_pi.pyx
cdef inline double recip_square(int i):
return 1. / (i * i)
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
```
現在運行配置文件腳本會產生:
```py
Sat Nov 7 18:10:11 2009 Profile.prof
10000004 function calls in 2.622 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 1.782 1.782 2.622 2.622 calc_pi.pyx:7(approx_pi)
10000000 0.840 0.000 0.840 0.000 calc_pi.pyx:4(recip_square)
1 0.000 0.000 2.622 2.622 {calc_pi.approx_pi}
1 0.000 0.000 2.622 2.622 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
```
那又給我們買了 1.8 秒。不是我們可以期待的戲劇性變化。為什么 recip_square 仍然在這個表中;應該是內聯的,不是嗎?這樣做的原因是,即使消除了函數調用,Cython 仍會生成分析代碼。讓我們告訴它不再介紹 recip_square;無論如何,我們無法使功能更快:
```py
# cython: profile=True
# calc_pi.pyx
cimport cython
@cython.profile(False)
cdef inline double recip_square(int i):
return 1. / (i * i)
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
```
運行它顯示了一個有趣的結果:
```py
Sat Nov 7 18:15:02 2009 Profile.prof
4 function calls in 0.089 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.089 0.089 0.089 0.089 calc_pi.pyx:10(approx_pi)
1 0.000 0.000 0.089 0.089 {calc_pi.approx_pi}
1 0.000 0.000 0.089 0.089 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
```
首先要注意的是速度的巨大提升:這個版本只占我們第一個 Cython 版本的 1/50。另請注意,recip_square 已經從我們想要的表中消失了。但最奇特和重要的變化是,about_pi 也變得更快。這是所有分析的問題:在配置文件運行中調用函數會給函數調用增加一定的開銷。這個開銷是**而不是**添加到被調用函數所花費的時間,而是加到**調用**函數所花費的時間。在這個例子中,在最后一次運行中,approx_pi 不需要 2.622 秒;但它調用了 recip_square 10000000 次,每次都需要稍微設置一下它的分析。這相當于大約 2.6 秒的大量時間損失。禁用經常調用的函數的分析現在揭示了 approx_pi 的實際時間;如果需要,我們現在可以繼續優化它。
這個分析教程到此結束。此代碼仍有一些改進空間。我們可以嘗試用 C stdlib 中的 sqrt 調用替換 approx_pi 中的冪運算符;但這并不一定比調用 pow(x,0.5)更快。
即便如此,我們在這里取得的成果非常令人滿意:我們提出了一個比原始 Python 版本快得多的解決方案,同時保留了功能和可讀性。
- Cython 3.0 中文文檔
- 入門
- Cython - 概述
- 安裝 Cython
- 構建 Cython 代碼
- 通過靜態類型更快的代碼
- Tutorials
- 基礎教程
- 調用 C 函數
- 使用 C 庫
- 擴展類型(又名.cdef 類)
- pxd 文件
- Caveats
- Profiling
- Unicode 和傳遞字符串
- 內存分配
- 純 Python 模式
- 使用 NumPy
- 使用 Python 數組
- 進一步閱讀
- 相關工作
- 附錄:在 Windows 上安裝 MinGW
- 用戶指南
- 語言基礎
- 擴展類型
- 擴展類型的特殊方法
- 在 Cython 模塊之間共享聲明
- 與外部 C 代碼連接
- 源文件和編譯
- 早期綁定速度
- 在 Cython 中使用 C ++
- 融合類型(模板)
- 將 Cython 代碼移植到 PyPy
- Limitations
- Cython 和 Pyrex 之間的區別
- 鍵入的內存視圖
- 實現緩沖協議
- 使用并行性
- 調試你的 Cython 程序
- 用于 NumPy 用戶的 Cython
- Pythran 作為 Numpy 后端