Python 效能分析器¶
原始碼: Lib/profile.py 和 Lib/pstats.py
效能分析器簡介¶
cProfile
和 profile
提供 Python 程式的確定性效能分析。 效能分析是一組統計資料,描述程式各個部分的執行頻率和執行時長。 這些統計資料可以透過 pstats
模組格式化為報告。
Python 標準庫提供了相同效能分析介面的兩種不同實現
cProfile
建議大多數使用者使用;它是一個 C 擴充套件,具有合理的開銷,使其適用於效能分析長時間執行的程式。 基於 Brett Rosen 和 Ted Czotter 貢獻的lsprof
。profile
,一個純 Python 模組,其介面被cProfile
模仿,但它會為效能分析的程式增加顯著的開銷。 如果你試圖以某種方式擴充套件效能分析器,使用此模組可能會更容易。 最初由 Jim Roskind 設計和編寫。
註解
效能分析器模組旨在為給定程式提供執行效能分析,而不是用於基準測試目的(為此,可以使用 timeit
來獲得相當準確的結果)。 這尤其適用於比較 Python 程式碼和 C 程式碼的基準測試:效能分析器會為 Python 程式碼引入開銷,但不會為 C 級函式引入開銷,因此 C 程式碼看起來比任何 Python 程式碼都快。
快速入門手冊¶
本節為那些“不想閱讀手冊”的使用者提供。 它提供了一個非常簡短的概述,並允許使用者快速對現有應用程式執行效能分析。
要分析一個帶有單個引數的函式,你可以這樣做
import cProfile
import re
cProfile.run('re.compile("foo|bar")')
(如果你的系統上沒有 cProfile
,則使用 profile
代替。)
上面的操作將執行 re.compile()
並列印如下效能分析結果
214 function calls (207 primitive calls) in 0.002 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.002 0.002 {built-in method builtins.exec}
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 __init__.py:250(compile)
1 0.000 0.000 0.001 0.001 __init__.py:289(_compile)
1 0.000 0.000 0.000 0.000 _compiler.py:759(compile)
1 0.000 0.000 0.000 0.000 _parser.py:937(parse)
1 0.000 0.000 0.000 0.000 _compiler.py:598(_code)
1 0.000 0.000 0.000 0.000 _parser.py:435(_parse_sub)
第一行表明監控了 214 次呼叫。 在這些呼叫中,有 207 次是原始呼叫,這意味著該呼叫不是透過遞迴引起的。 下一行:Ordered by: cumulative time
表明輸出按 cumtime
值排序。 列標題包括
- ncalls
表示呼叫次數。
- tottime
表示在給定函式中花費的總時間(不包括呼叫子函式所花費的時間)
- percall
是
tottime
除以ncalls
的商- cumtime
表示在此函式和所有子函式中花費的累積時間(從呼叫到退出)。 這個數字對於遞迴函式來說是準確的即使。
- percall
是
cumtime
除以原始呼叫的商- filename:lineno(function)
提供每個函式的相應資料
當第一列中有兩個數字時(例如 3/1
),表示該函式是遞迴的。 第二個值是原始呼叫的次數,前者是呼叫總次數。 請注意,當函式不遞迴時,這兩個值是相同的,並且僅列印單個數字。
你可以透過為 run()
函式指定檔名,將結果儲存到檔案中,而不是在效能分析執行結束時列印輸出
import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')
pstats.Stats
類從檔案中讀取效能分析結果並以各種方式對其進行格式化。
檔案 cProfile
和 profile
也可以作為指令碼呼叫來分析另一個指令碼。 例如
python -m cProfile [-o output_file] [-s sort_order] (-m module | myscript.py)
-o
將效能分析結果寫入檔案而不是標準輸出
-s
指定一個 sort_stats()
排序值來對輸出進行排序。 這僅在未提供 -o
時適用。
-m
指定正在分析模組而不是指令碼。
3.7 版本新增: 向 cProfile
添加了 -m
選項。
3.8 版本新增: 向 profile
添加了 -m
選項。
pstats
模組的 Stats
類有多種方法來操作和列印儲存到效能分析結果檔案中的資料
import pstats
from pstats import SortKey
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()
strip_dirs()
方法刪除了所有模組名稱中多餘的路徑。 sort_stats()
方法根據列印的標準模組/行/名稱字串對所有條目進行排序。 print_stats()
方法打印出所有統計資料。 你可以嘗試以下排序呼叫
p.sort_stats(SortKey.NAME)
p.print_stats()
第一個呼叫實際上會按函式名稱對列表進行排序,第二個呼叫將打印出統計資訊。 以下是一些有趣的呼叫,可以嘗試
p.sort_stats(SortKey.CUMULATIVE).print_stats(10)
這將按函式中的累積時間對效能分析進行排序,然後僅列印十個最重要的行。 如果你想了解哪些演算法正在佔用時間,那麼你會使用上面的行。
如果你想看看哪些函式迴圈了很多次,並且花費了很多時間,你會這樣做
p.sort_stats(SortKey.TIME).print_stats(10)
根據每個函式中花費的時間進行排序,然後打印出前十個函式的統計資訊。
你也可以嘗試
p.sort_stats(SortKey.FILENAME).print_stats('__init__')
這將按檔名對所有統計資訊進行排序,然後僅列印類初始化方法的統計資訊(因為它們的拼寫中包含 __init__
)。 作為最後一個示例,你可以嘗試
p.sort_stats(SortKey.TIME, SortKey.CUMULATIVE).print_stats(.5, 'init')
此行使用時間作為主鍵,累積時間作為次鍵對統計資訊進行排序,然後列印一些統計資訊。 具體來說,列表首先縮小到其原始大小的 50%(即:.5
),然後只保留包含 init
的行,並列印該子子列表。
如果你想知道哪些函式呼叫了上面的函式,你現在可以(p
仍然根據最後一個標準排序)執行以下操作
p.print_callers(.5, 'init')
你會得到每個列出函式的呼叫者列表。
如果你想要更多功能,你必須閱讀手冊,或者猜測以下函式的功能
p.print_callees()
p.add('restats')
作為指令碼呼叫時,pstats
模組是一個統計資訊瀏覽器,用於讀取和檢查效能分析轉儲。 它具有簡單的面向行的介面(使用 cmd
實現)和互動式幫助。
profile
和 cProfile
模組參考¶
- profile.run(command, filename=None, sort=-1)¶
此函式接受一個可以傳遞給
exec()
函式的引數,以及一個可選的檔名。在所有情況下,此例程都會執行exec(command, __main__.__dict__, __main__.__dict__)
並從執行中收集效能分析統計資訊。如果不存在檔名,則此函式會自動建立一個
Stats
例項並列印一個簡單的效能分析報告。如果指定了排序值,則將其傳遞給此Stats
例項以控制結果的排序方式。
- profile.runctx(command, globals, locals, filename=None, sort=-1)¶
此函式類似於
run()
,但添加了引數以提供 command 字串的全域性和區域性對映。此例程執行exec(command, globals, locals)
並像上面的
run()
函式一樣收集效能分析統計資訊。
- class profile.Profile(timer=None, timeunit=0.0, subcalls=True, builtins=True)¶
通常只有在需要比
cProfile.run()
函式提供的更精確的效能分析控制時才使用此類。可以透過 timer 引數提供自定義計時器來測量程式碼執行的時間。這必須是一個返回表示當前時間的單個數字的函式。如果該數字是整數,則 timeunit 指定一個乘數,該乘數指定每個時間單位的持續時間。例如,如果計時器返回以千分之一秒為單位測量的時間,則時間單位將為
.001
。直接使用
Profile
類允許格式化效能分析結果而無需將效能分析資料寫入檔案import cProfile, pstats, io from pstats import SortKey pr = cProfile.Profile() pr.enable() # ... do something ... pr.disable() s = io.StringIO() sortby = SortKey.CUMULATIVE ps = pstats.Stats(pr, stream=s).sort_stats(sortby) ps.print_stats() print(s.getvalue())
Profile
類也可以用作上下文管理器(僅在cProfile
模組中支援。請參閱 上下文管理器型別)import cProfile with cProfile.Profile() as pr: # ... do something ... pr.print_stats()
在 3.8 版本中變更: 添加了上下文管理器支援。
- create_stats()¶
停止收集效能分析資料,並將結果在內部記錄為當前效能分析。
- print_stats(sort=-1)¶
基於當前效能分析建立一個
Stats
物件,並將結果列印到標準輸出。sort 引數指定顯示的統計資訊的排序順序。它接受單個鍵或鍵的元組以啟用多級排序,如
Stats.sort_stats
中所示。3.13 版本新增:
print_stats()
現在接受鍵的元組。
- dump_stats(filename)¶
將當前效能分析的結果寫入 filename。
- runcall(func, /, *args, **kwargs)¶
分析
func(*args, **kwargs)
的效能
請注意,只有當呼叫的命令/函式實際返回時,效能分析才會起作用。如果直譯器被終止(例如,在呼叫的命令/函式執行期間透過 sys.exit()
呼叫),則不會列印任何效能分析結果。
Stats
類¶
效能分析器資料的分析是使用 Stats
類完成的。
- class pstats.Stats(*filenames or profile, stream=sys.stdout)¶
此類建構函式從 filename (或檔名列表) 或從
Profile
例項建立一個“統計物件”例項。輸出將列印到 stream 指定的流。上述建構函式選擇的檔案必須由相應版本的
profile
或cProfile
建立。具體來說,不保證此效能分析器的未來版本的檔案相容性,並且與其他效能分析器生成的檔案或在不同作業系統上執行的同一性能分析器生成的檔案不相容。如果提供了多個檔案,則將合併相同函式的所有統計資訊,以便在單個報告中考慮多個程序的總體檢視。如果需要將其他檔案與現有Stats
物件中的資料組合,可以使用add()
方法。除了從檔案中讀取效能分析資料,還可以使用
cProfile.Profile
或profile.Profile
物件作為效能分析資料來源。Stats
物件具有以下方法:- strip_dirs()¶
Stats
類的此方法會刪除檔名中所有前導路徑資訊。它對於縮小列印輸出的大小以適應(接近)80列非常有用。此方法會修改物件,並且剝離的資訊會丟失。執行剝離操作後,該物件被認為其條目處於“隨機”順序,就像剛完成物件初始化和載入時一樣。如果strip_dirs()
導致兩個函式名稱無法區分(它們位於同一檔案的同一行,並且具有相同的函式名稱),則這兩個條目的統計資訊將累積到單個條目中。
- add(*filenames)¶
Stats
類的此方法將額外的效能分析資訊累積到當前的效能分析物件中。其引數應引用由相應版本的profile.run()
或cProfile.run()
建立的檔名。具有相同名稱(即:檔案、行、名稱)的函式的統計資訊會自動累積到單個函式統計資訊中。
- dump_stats(filename)¶
將載入到
Stats
物件中的資料儲存到名為 filename 的檔案中。如果該檔案不存在,則會建立該檔案;如果該檔案已存在,則會覆蓋該檔案。這等效於profile.Profile
和cProfile.Profile
類中同名的方法。
- sort_stats(*keys)¶
此方法會根據提供的標準對
Stats
物件進行排序,從而修改該物件。該引數可以是字串或 SortKey 列舉,用於標識排序的基礎(例如:'time'
、'name'
、SortKey.TIME
或SortKey.NAME
)。SortKey 列舉引數的優點在於它比字串引數更健壯且更不易出錯。當提供多個鍵時,如果之前選擇的所有鍵都相等,則將使用其他鍵作為次要標準。例如,
sort_stats(SortKey.NAME, SortKey.FILE)
將根據函式名稱對所有條目進行排序,並透過按檔名排序來解決所有聯絡(相同的函式名稱)。對於字串引數,只要縮寫是明確的,就可以使用任何鍵名稱的縮寫。
以下是有效的字串和 SortKey:
有效的字串引數
有效的列舉引數
含義
'calls'
SortKey.CALLS
呼叫計數
'cumulative'
SortKey.CUMULATIVE
累積時間
'cumtime'
不適用
累積時間
'file'
不適用
檔名
'filename'
SortKey.FILENAME
檔名
'module'
不適用
檔名
'ncalls'
不適用
呼叫計數
'pcalls'
SortKey.PCALLS
原始呼叫計數
'line'
SortKey.LINE
行號
'name'
SortKey.NAME
函式名稱
'nfl'
SortKey.NFL
名稱/檔案/行
'stdname'
SortKey.STDNAME
標準名稱
'time'
SortKey.TIME
內部時間
'tottime'
不適用
內部時間
請注意,所有基於統計資訊的排序都按降序排列(將最耗時的項放在最前面),而名稱、檔案和行號搜尋則按升序排列(按字母順序)。
SortKey.NFL
和SortKey.STDNAME
之間的細微區別在於,標準名稱是對列印的名稱進行排序,這意味著嵌入的行號會以奇特的方式進行比較。例如,如果檔名相同,則行 3、20 和 40 將按字串順序 20、3 和 40 顯示。相反,SortKey.NFL
會對行號進行數值比較。實際上,sort_stats(SortKey.NFL)
與sort_stats(SortKey.NAME, SortKey.FILENAME, SortKey.LINE)
相同。出於向後相容性的原因,允許使用數值引數
-1
、0
、1
和2
。它們分別被解釋為'stdname'
、'calls'
、'time'
和'cumulative'
。如果使用這種舊樣式格式(數值),則只會使用一個排序鍵(數值鍵),並且會默默忽略其他引數。3.7 版本新增: 添加了 SortKey 列舉。
- print_stats(*restrictions)¶
Stats
類的此方法會按照profile.run()
定義中所述的方式列印報表。列印的順序基於對物件執行的最後一個
sort_stats()
操作(受add()
和strip_dirs()
中的警告影響)。提供的引數(如果有)可用於將列表限制為重要的條目。最初,該列表被視為已分析的函式的完整集合。每個限制要麼是一個整數(用於選擇行計數),要麼是一個介於 0.0 和 1.0(含)之間的小數(用於選擇行百分比),要麼是一個將解釋為正則表示式的字串(用於模式匹配列印的標準名稱)。如果提供了多個限制,則會依次應用這些限制。例如
print_stats(.1, 'foo:')
將首先將列印限制為列表的前 10%,然後僅列印屬於檔名
.*foo:
的函式。相反,命令print_stats('foo:', .1)
會將列表限制為所有具有檔名
.*foo:
的函式,然後繼續僅列印其中的前 10%。
- print_callers(*restrictions)¶
Stats
類的此方法會列印效能分析資料庫中呼叫每個函式的所有函式的列表。排序與print_stats()
提供的排序相同,並且限制引數的定義也相同。每個呼叫者都會在其自己的行上報告。格式會因生成統計資訊的效能分析器而略有不同
- print_callees(*restrictions)¶
Stats
類的此方法會列印指示函式呼叫的所有函式的列表。除了呼叫方向的這種反轉(即:被呼叫與被呼叫者),引數和排序與print_callers()
方法相同。
- get_stats_profile()¶
此方法返回一個 StatsProfile 例項,其中包含函式名稱到 FunctionProfile 例項的對映。每個 FunctionProfile 例項都儲存與其函式分析相關的資訊,例如函式執行所花費的時間、呼叫次數等。
3.9 版本新增: 添加了以下資料類:StatsProfile、FunctionProfile。 添加了以下函式:get_stats_profile。
什麼是確定性分析?¶
確定性分析 旨在反映所有函式呼叫、函式返回和異常事件都受到監控的事實,並且對這些事件之間的時間間隔(在此期間使用者程式碼正在執行)進行精確的計時。相比之下,統計分析(本模組不進行)隨機取樣有效的指令指標,並推斷時間花費在哪裡。後一種技術通常涉及較少的開銷(因為程式碼不需要進行檢測),但僅提供時間花費在何處的相對指示。
在 Python 中,由於執行過程中存在活動的直譯器,因此不需要檢測程式碼即可進行確定性分析。Python 會自動為每個事件提供一個鉤子(可選回撥)。此外,Python 的解釋性質往往會給執行增加很多開銷,以至於確定性分析在典型應用程式中往往只會增加很小的處理開銷。結果是確定性分析並不那麼昂貴,但提供了關於 Python 程式執行的廣泛執行時統計資訊。
呼叫計數統計資訊可用於識別程式碼中的錯誤(令人驚訝的計數),並識別可能的內聯擴充套件點(高呼叫計數)。內部時間統計資訊可用於識別應仔細最佳化的“熱迴圈”。累積時間統計資訊應用於識別演算法選擇中的高階錯誤。請注意,此分析器中對累積時間的特殊處理允許將演算法的遞迴實現的統計資訊與迭代實現的統計資訊直接進行比較。
侷限性¶
一個侷限性與計時資訊的準確性有關。確定性分析器在準確性方面存在一個根本性問題。最明顯的限制是底層“時鐘”的滴答速率(通常)約為 0.001 秒。因此,任何測量的精度都不會高於底層時鐘。如果進行足夠的測量,則“誤差”將趨於平均化。不幸的是,消除第一個誤差會導致第二個誤差源。
第二個問題是,從事件被分派到分析器的呼叫實際獲取時鐘狀態需要“一段時間”。同樣,當從分析器事件處理程式退出時,從獲取時鐘值(然後將其儲存起來)到使用者的程式碼再次執行時,也存在一定的延遲。因此,被多次呼叫或呼叫許多函式的函式通常會累積此錯誤。以這種方式累積的誤差通常小於時鐘的精度(小於一個時鐘滴答),但它可以累積並變得非常顯著。
這個問題在使用 profile
時比在使用開銷較低的 cProfile
時更為重要。因此,profile
提供了一種為給定平臺校準自身的方法,以便可以機率性地(平均而言)消除此誤差。在分析器校準後,它將更加準確(在最小二乘意義上),但有時會產生負數(當呼叫計數異常低,並且機率之神與您作對時 :-))。請不要對分析中的負數感到恐慌。只有在您校準了分析器後,它們才應該出現,並且結果實際上比沒有校準更好。
校準¶
profile
模組的分析器從每個事件處理時間中減去一個常數,以補償呼叫時間函式和儲存結果的開銷。預設情況下,該常數為 0。可以使用以下過程來獲得給定平臺的更好的常數(請參見 侷限性)。
import profile
pr = profile.Profile()
for i in range(5):
print(pr.calibrate(10000))
該方法執行由引數給出的 Python 呼叫次數,直接執行一次,並在分析器下再次執行,測量兩者的時間。然後,它計算每個分析器事件的隱藏開銷,並將其作為浮點數返回。例如,在執行 macOS 的 1.8Ghz Intel Core i5 上,並使用 Python 的 time.process_time() 作為計時器,神奇的數字約為 4.04e-6。
此練習的目的是獲得相當一致的結果。如果您的計算機非常快,或者您的計時器函式解析度較差,則您可能必須傳遞 100000,甚至 1000000,才能獲得一致的結果。
當您獲得一致的答案時,有三種使用方法
import profile
# 1. Apply computed bias to all Profile instances created hereafter.
profile.Profile.bias = your_computed_bias
# 2. Apply computed bias to a specific Profile instance.
pr = profile.Profile()
pr.bias = your_computed_bias
# 3. Specify computed bias in instance constructor.
pr = profile.Profile(bias=your_computed_bias)
如果您有選擇,最好選擇較小的常數,這樣您的結果在分析統計中“不太可能”顯示為負數。
使用自定義計時器¶
如果您想更改確定當前時間的方式(例如,強制使用掛鐘時間或經過的程序時間),請將您想要的計時函式傳遞給 Profile
類建構函式
pr = profile.Profile(your_time_func)
然後,生成的分析器將呼叫 your_time_func
。根據您使用的是 profile.Profile
還是 cProfile.Profile
,your_time_func
的返回值將被不同地解釋
profile.Profile
your_time_func
應該返回一個數字,或一個數字列表,其總和為當前時間(類似於os.times()
返回的內容)。如果該函式返回一個時間數字,或者返回的數字列表的長度為 2,則您將獲得一個特別快速的排程例程版本。請注意,您應該為您選擇的計時器函式校準分析器類(請參見 校準)。對於大多數機器來說,返回單個整數值的計時器在分析期間的低開銷方面將提供最佳結果。(
os.times()
非常糟糕,因為它返回浮點值的元組)。如果您想以最乾淨的方式替換更好的計時器,請派生一個類並硬連線一個最適合您的計時器呼叫的替換排程方法,以及適當的校準常數。cProfile.Profile
your_time_func
應該返回一個數字。如果它返回整數,您還可以使用第二個引數呼叫類建構函式,該引數指定一個時間單位的實際持續時間。例如,如果your_integer_time_func
返回以千分之一秒為單位的時間,則您將按如下方式構造Profile
例項pr = cProfile.Profile(your_integer_time_func, 0.001)
由於
cProfile.Profile
類無法校準,因此自定義計時器函式應謹慎使用,並應儘可能快。為了獲得使用自定義計時器的最佳結果,可能需要在內部_lsprof
模組的 C 原始碼中對其進行硬編碼。
Python 3.3 在 time
中添加了幾個新函式,可用於精確測量程序或掛鐘時間。例如,請參閱 time.perf_counter()
。