當前位置:
首頁 > 知識 > Python 性能優化

Python 性能優化

(點擊

上方藍字

,快速關注我們)




來源:xybaby


www.cnblogs.com/xybaby/p/6510941.html


如有好文章投稿,請點擊 → 這裡了解詳情




正文




注意:本文除非特殊指明,」python「都是代表CPython,即C語言實現的標準python,且本文所討論的是版本為2.7的CPython。




python為什麼性能差:




當我們提到一門編程語言的效率時:通常有兩層意思,

第一是開發效率

,這是對程序員而言,完成編碼所需要的時間;

另一個是運行效率

,這是對計算機而言,完成計算任務所需要的時間。編碼效率和運行效率往往是魚與熊掌的關係,是很難同時兼顧的。不同的語言會有不同的側重,python語言毫無疑問更在乎編碼效率,life is short,we use python。



雖然使用python的編程人員都應該接受其運行效率低的事實,但python在越多越來的領域都有廣泛應用,比如科學計算 、web伺服器等。程序員當然也希望python能夠運算得更快,希望python可以更強大。




首先,python相比其他語言具體有多慢,這個不同場景和測試用例,結果肯定是不一樣的。這個網址給出了不同語言在各種case下的性能對比,這一頁是python3和C++的對比,下面是兩個case:







python運算效率低,具體是什麼原因呢,下列羅列一些




第一:python是動態語言



一個變數所指向對象的類型在運行時才確定,編譯器做不了任何預測,也就無從優化。舉一個簡單的例子: r = a + b。 a和b相加,但a和b的類型在運行時才知道,對於加法操作,不同的類型有不同的處理,所以每次運行的時候都會去判斷a和b的類型,然後執行對應的操作。而在靜態語言如C++中,編譯的時候就確定了運行時的代碼。




另外一個例子是屬性查找,關於具體的查找順序在《python屬性查找》中有詳細介紹。簡而言之,訪問對象的某個屬性是一個非常複雜的過程,而且通過同一個變數訪問到的python對象還都可能不一樣(參見Lazy property的例子)。而在C語言中,訪問屬性用對象的地址加上屬性的偏移就可以了。




第二:python是解釋執行

,但是不支持JIT(just in time compiler)。雖然大名鼎鼎的google曾經嘗試Unladen Swallow 這個項目,但最終也折了。




第三:python中一切都是對象

,每個對象都需要維護引用計數,增加了額外的工作。



第四:python GIL




GIL是Python最為詬病的一點,因為GIL,python中的多線程並不能真正的並發。如果是在IO bound的業務場景,這個問題並不大,但是在CPU BOUND的場景,這就很致命了。所以筆者在工作中使用python多線程的情況並不多,一般都是使用多進程(pre fork),或者在加上協程。即使在單線程,GIL也會帶來很大的性能影響,因為python每執行100個opcode(默認,可以通過sys.setcheckinterval()設置)就會嘗試線程的切換,具體的源代碼在ceval.c::PyEval_EvalFrameEx。




第五:垃圾回收

,這個可能是所有具有垃圾回收的編程語言的通病。python採用標記和分代的垃圾回收策略,每次垃圾回收的時候都會中斷正在執行的程序,造成所謂的頓卡。infoq上有一篇文章,提到禁用Python的GC機制後,Instagram性能提升了10%。感興趣的讀者可以去細讀。




Be pythonic




我們都知道 過早的優化是罪惡之源,一切優化都需要基於profile。但是,作為一個python開發者應該要pythonic,而且pythonic的代碼往往比non-pythonic的代碼效率高一些,比如:






  • 使用迭代器iterator,for example:




dict的iteritems 而不是items(同itervalues,iterkeys)


使用generator,特別是在循環中可能提前break的情況






  • 判斷是否是同一個對象使用 is 而不是 ==



  • 判斷一個對象是否在一個集合中,使用set而不是list



  • 利用短路求值特性,把「短路」概率過的邏輯表達式寫在前面。其他的lazy ideas也是可以的



  • 對於大量字元串的累加,使用join操作



  • 使用for else(while else)語法



  • 交換兩個變數的值使用: a, b = b, a




基於profile的優化




即使我們的代碼已經非常pythonic了,但可能運行效率還是不能滿足預期。我們也知道80/20定律,絕大多數的時間都耗費在少量的代碼片段裡面了,優化的關鍵在於找出這些瓶頸代碼。方式很多:到處加log列印時間戳、或者將懷疑的函數使用timeit進行單獨測試,但最有效的是使用profile工具。



python profilers




對於python程序,比較出名的profile工具有三個:profile、cprofile和hotshot。其中profile是純python語言實現的,Cprofile將profile的部分實現native化,hotshot也是C語言實現,hotshot與Cprofile的區別在於:hotshot對目標代碼的運行影響較小,代價是更多的後處理時間,而且hotshot已經停止維護了。需要注意的是,profile(Cprofile hotshot)只適合單線程的python程序。




對於多線程,可以使用yappi,yappi不僅支持多線程,還可以精確到CPU時間




對於協程(greenlet),可以使用greenletprofiler,基於yappi修改,用greenlet context hook住thread context



下面給出一段編造的」效率低下「的代碼,並使用Cprofile來說明profile的具體方法以及我們可能遇到的性能瓶頸。





# -*- coding: UTF-8 -*-


 


from

cProfile

import

Profile


import

math


def

foo

()

:


    

return

foo1

()


 


def

foo1

()

:


    

return

foo2

()


 


def

foo2

()

:


    

return

foo3

()


 


def

foo3

()

:


    

return

foo4

()


 


def

foo4

()

:


    

return

"this call tree seems ugly, but it always happen"


 


def

bar

()

:


    

ret

=

0


    

for

i

in

xrange

(

10000

)

:


        

ret

+=

i

*

i

+

math

.

sqrt

(

i

)


    

return

ret


 


def

main

()

:


    

for

i

in

range

(

100000

)

:


        

if

i

%

10000

==

0

:


            

bar

()


        

else

:


            

foo

()


 


if

__name__

==

"__main__"

:


    

prof

=

Profile

()


    

prof

.

runcall

(

main

)


    

prof

.

print_stats

()


    

#prof.dump_stats("test.prof") # dump profile result to test.prof


 


code

for

profile




運行結果如下:







對於上面的的輸出,每一個欄位意義如下:




ncalls 函數總的調用次數




tottime 函數內部(不包括子函數)的佔用時間




percall(第一個) tottime/ncalls




cumtime 函數包括子函數所佔用的時間




percall(第二個)cumtime/ncalls




filename:lineno(function) 文件:行號(函數)




代碼中的輸出非常簡單,事實上可以利用pstat,讓profile結果的輸出多樣化,具體可以參見官方文檔python profiler。




profile GUI tools




雖然Cprofile的輸出已經比較直觀,但我們還是傾向於保存profile的結果,然後用圖形化的工具來從不同的維度來分析,或者比較優化前後的代碼。查看profile結果的工具也比較多,比如,visualpytune、qcachegrind、runsnakerun,本文用visualpytune做分析。對於上面的代碼,按照注釋生成修改後重新運行生成test.prof文件,用visualpytune直接打開就可以了,如下:







欄位的意義與文本輸出基本一致,不過便捷性可以點擊欄位名排序。左下方列出了當前函數的calller(調用者),右下方是當前函數內部與子函數的時間佔用情況。上如是按照cumtime(即該函數內部及其子函數所佔的時間和)排序的結果。




造成性能瓶頸的原因通常是

高頻調用的函數、單次消耗非常高的函數、或者二者的結合

。在我們前面的例子中,foo就屬於高頻調用的情況,bar屬於單次消耗非常高的情況,這都是我們需要優化的重點。




python-profiling-tools中介紹了qcachegrind和runsnakerun的使用方法,這兩個colorful的工具比visualpytune強大得多。具體的使用方法請參考原文,下圖給出test.prof用qcachegrind打開的結果。







qcachegrind確實要比visualpytune強大。從上圖可以看到,大致分為三部:。第一部分同visualpytune類似,是每個函數佔用的時間,其中Incl等同於cumtime, Self等同於tottime。第二部分和第三部分都有很多標籤,不同的標籤標示從不同的角度來看結果,如圖上所以,第三部分的「call graph」展示了該函數的call tree並包含每個子函數的時間百分比,一目了然。




profile針對優化




知道了熱點,就可以進行針對性的優化,而這個優化往往根具體的業務密切相關,沒用萬能鑰匙,具體問題,具體分析。個人經驗而言,最有效的優化是找產品經理討論需求,可能換一種方式也能滿足需求,少者稍微折衷一下產品經理也能接受。次之是修改代碼的實現,比如之前使用了一個比較通俗易懂但效率較低的演算法,如果這個演算法成為了性能瓶頸,那就考慮換一種效率更高但是可能難理解的演算法、或者使用dirty Flag模式。對於這些同樣的方法,需要結合具體的案例,本文不做贅述。




接下來結合python語言特性,介紹一些讓python代碼不那麼pythonic,但可以提升性能的一些做法




第一:減少函數的調用層次




每一層函數調用都會帶來不小的開銷,特別對於調用頻率高,但單次消耗較小的calltree,多層的函數調用開銷就很大,這個時候可以考慮將其展開。




對於之前調到的profile的代碼,foo這個call tree非常簡單,但頻率高。修改代碼,增加一個plain_foo()函數, 直接返回最終結果,關鍵輸出如下:







跟之前的結果對比:







可以看到,優化了差不多3倍。




第二:優化屬性查找




上面提到,python 的屬性查找效率很低,如果在一段代碼中頻繁訪問一個屬性(比如for循環),那麼可以考慮用局部變數代替對象的屬性。




第三:關閉GC




在本文的第一章節已經提到,關閉GC可以提升python的性能,GC帶來的頓卡在實時性要求比較高的應用場景也是難以接受的。但關閉GC並不是一件容易的事情。我們知道python的引用計數只能應付沒有循環引用的情況,有了循環引用就需要靠GC來處理。在python語言中, 寫出循環引用非常容易。比如:





case

1


  

a

,

b

=

SomeClass

(),

SomeClass

()


  

a

.

b

,

b

.

a

=

b

,

a


   


case

2


  

lst

=

[]


  

lst

.

append

(

lst

)



case

3


  

self

.

handler

=

self

.

some_func




當然,大家可能說,誰會這麼傻,寫出這樣的代碼,是的,上面的代碼太明顯,當中間多幾個層級之後,就會出現「間接」的循環應用。在python的標準庫 collections裡面的OrderedDict就是case2:







要解決循環引用,第一個辦法是使用弱引用(weakref),第二個是手動解循環引用。




第四:setcheckinterval




如果程序確定是單線程,那麼修改checkinterval為一個更大的值,這裡有介紹。




第五:使用__slots__




slots最主要的目的是用來節省內存,但是也能一定程度上提高性能。我們知道定義了__slots__的類,對某一個實例都會預留足夠的空間,也就不會再自動創建__dict__。當然,使用__slots__也有許多注意事項,最重要的一點,繼承鏈上的所有類都必須定義__slots__,python doc有詳細的描述。下面看一個簡單的測試例子:





class

BaseSlots

(

object

)

:


    

__slots__

=

[

"e"

,

"f"

,

"g"

]


 


class

Slots

(

BaseSlots

)

:


    

__slots__

=

[

"a"

,

"b"

,

"c"

,

"d"

]


    

def

__init__

(

self

)

:


        

self

.

a

=

self

.

b

=

self

.

c

=

self

.

d

=

self

.

e

=

self

.

f

  =

self

.

g

=

0


 


class

BaseNoSlots

(

object

)

:


        

pass


 


class

NoSlots

(

BaseNoSlots

)

:


    

def

__init__

(

self

)

:


        

super

(

NoSlots

,

self

).

__init__

()


        

self

.

a

=

self

.

b

=

self

.

c

=

self

.

d

=

self

.

e

=

self

.

f

  =

self

.

g

=

0


 


def

log_time

(

s

)

:


    

begin

=

time

.

time

()


    

for

i

in

xrange

(

10000000

)

:


        

s

.

a

,

s

.

b

,

s

.

c

,

s

.

d

,

s

.

e

,

s

.

f

,

s

.

g


    

return

time

.

time

()

-

begin


 


if

__name__

==

"__main__"

:


    

print

"Slots cost"

,

log_time

(

Slots

())


    

print

"NoSlots cost"

,

log_time

(

NoSlots

())




輸出結果:





Slots cost 3.12999987602


NoSlots cost 3.48100018501




python C擴展




也許通過profile,我們已經找到了性能熱點,但這個熱點就是要運行大量的計算,而且沒法cache,沒法省略。。。這個時候就該python的C擴展出馬了,

C擴展就是把部分python代碼用C或者C++重新實現,然後編譯成動態鏈接庫,提供介面給其它python代碼調用。

由於C語言的效率遠遠高於python代碼,所以使用C擴展是非常普遍的做法,比如我們前面提到的cProfile就是基於_lsprof.so的一層封裝。python的大所屬對性能有要求的庫都使用或者提供了C擴展,如gevent、protobuff、bson。




筆者曾經測試過純python版本的bson和cbson的效率,在綜合的情況下,cbson快了差不多10倍!




python的C擴展也是一個非常複雜的問題,本文僅給出一些注意事項:




第一:注意引用計數的正確管理




這是最難最複雜的一點。我們都知道python基於指針技術來管理對象的生命周期,如果在擴展中引用計數出了問題,那麼要麼是程序崩潰,要麼是內存泄漏。更要命的是,引用計數導致的問題很難debug。。。




C擴展中關於引用計數最關鍵的三個詞是:steal reference,borrowed reference,new reference。建議編寫擴展代碼之前細讀python的官方文檔。




第二:C擴展與多線程




這裡的多線程是指在擴展中new出來的C語言線程,而不是python的多線程,出了python doc裡面的介紹,也可以看看《python cookbook》的相關章節。




第三:C擴展應用場景




僅適合與業務代碼的關係不那麼緊密的邏輯,如果一段代碼大量業務相關的對象 屬性的話,是很難C擴展的




將C擴展封裝成python代碼可調用的介面的過程稱之為binding,Cpython本身就提供了一套原生的API,雖然使用最為廣泛,但該規範比較複雜。很多第三方庫做了不同程度的封裝,以便開發者使用,比如boost.python、cython、ctypes、cffi(同時支持pypy cpython),具體怎麼使用可以google。




beyond CPython




儘管python的性能差強人意,但是其易學易用的特性還是贏得越來越多的使用者,業界大牛也從來沒有放棄對python的優化。這裡的優化是對python語言設計上、或者實現上的一些反思或者增強。這些優化項目一些已經夭折,一些還在進一步改善中,在這個章節介紹目前還不錯的一些項目。




cython




前面提到cython可以用到binding c擴展,但是其作用遠遠不止這一點。




Cython的主要目的是加速python的運行效率,但是又不像上一章節提到的C擴展那麼複雜。在Cython中,寫C擴展和寫python代碼的複雜度差不多(多虧了Pyrex)。Cython是python語言的超集,增加了對C語言函數調用和類型聲明的支持。從這個角度來看,cython將動態的python代碼轉換成靜態編譯的C代碼,這也是cython高效的原因。使用cython同C擴展一樣,需要編譯成動態鏈接庫,在linux環境下既可以用命令行,也可以用distutils。




如果想要系統學習cython,建議從cython document入手,文檔寫得很好。下面通過一個簡單的示例來展示cython的使用方法和性能(linux環境)。




首先,安裝cython:





pip install Cython




下面是測試用的python代碼,可以看到這兩個case都是運算複雜度比較高的例子:





def f():


    return

x

**

2

-

x


 


def

integrate_f

(

a

,

b

,

N

)

:


    

s

=

0


    

dx

=

(

b

-

a

)

/

N


    

for

i

in

range

(

N

)

:


        

s

+=

f

(

a

+

i

*

dx

)


    

return

s

*

dx


 


def

main

()

:


    

import

time


    

begin

=

time

.

time

()


    

for

i

in

xrange

(

10000

)

:


        

for

i

in

xrange

(

100

)

:

f

(

10

)


    

print

"call f cost:"

,

time

.

time

()

-

begin


    

begin

=

time

.

time

()


    

for

i

in

xrange

(

10000

)

:


        

integrate_f

(

1.0

,

100.0

,

1000

)


    

print

"call integrate_f cost:"

,

time

.

time

()

-

begin


 


if

__name__

==

"__main__"

:


    

main

()




運行結果:





call f cost: 0.215116024017


call integrate_f cost: 4.33698010445




不改動任何python代碼也可以享受到cython帶來的性能提升,具體做法如下:






  • step1:將文件名(cython_example.py)改為cython_example.pyx



  • step2:增加一個setup.py文件,添加一下代碼:





from

distutils.core

import

setup


from

Cython

.

Build

import

cythonize


 


setup

(


  

name

=

"cython_example"

,


  

ext_modules

=

cythonize

(

"cython_example.pyx"

),


)






  • step3:執行python setup.py build_ext –inplace







可以看到 增加了兩個文件,對應中間結果和最後的動態鏈接庫






  • step4:執行命令 python -c 「import cython_example;cython_example.main()」(注意: 保證當前環境下已經沒有 cython_example.py)




運行結果:





call f cost: 0.0874309539795


call integrate_f cost: 2.92381191254




性能提升了大概兩倍,我們再來試試cython提供的靜態類型(static typing),修改cython_example.pyx的核心代碼,替換f()和integrate_f()的實現如下:





def

f

(

double

x

)

:

# 參數靜態類型


    

return

x

**

2

-

x


 


def

integrate_f

(

double

a

,

double

b

,

int

N

)

:


    

cdef int

i


    

cdef

double

s

,

dx


    

s

=

0


    

dx

=

(

b

-

a

)

/

N


    

for

i

in

range

(

N

)

:


        

s

+=

f

(

a

+

i

*

dx

)


    

return

s

*

dx




然後重新運行上面的第三 四步:結果如下





call f cost: 0.042387008667


call integrate_f cost: 0.958620071411




上面的代碼,只是對參數引入了靜態類型判斷,下面對返回值也引入靜態類型判斷。




替換f()和integrate_f()的實現如下:





cdef

double

f

(

double

x

)

:

# 返回值也有類型判斷


    

return

x

**

2

-

x


 


cdef double integrate_f

(

double

a

,

double

b

,

int

N

)

:


    

cdef int

i


    

cdef

double

s

,

dx


    

s

=

0


    

dx

=

(

b

-

a

)

/

N


    

for

i

in

range

(

N

)

:


        

s

+=

f

(

a

+

i

*

dx

)


    

return

s

*

dx




然後重新運行上面的第三 四步:結果如下





call f cost: 1.19209289551e-06


call integrate_f cost: 0.187038183212




Amazing!




pypy




pypy是CPython的一個替代實現,其最主要的優勢就是pypy的速度,下面是官網的測試結果:







在實際項目中測試,pypy大概比cpython要快3到5倍!pypy的性能提升來自JIT Compiler。在前文提到google的Unladen Swallow 項目也是想在CPython中引入JIT,在這個項目失敗後,很多開發人員都開始加入pypy的開發和優化。另外pypy佔用的內存更少,而且支持stackless,基本等同於協程。




pypy的缺點在於對C擴展方面支持的不太好,需要使用CFFi來做binding。對於使用廣泛的library來說,一般都會支持pypy,但是小眾的、或者自行開發的C擴展就需要重新封裝了。




ChangeLog




2017.03.10 增加了對__slots__的介紹




references






  • 編程語言benchmark



  • python屬性查找



  • python profiler



  • yappi



  • greenletprofiler



  • python-profiling-tools



  • python C API



  • cython



  • Pyrex



  • cython document



  • pypy




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

Fedora 需要幾年時間才能從 Python 2 切換到 Python 3
圖解機器學習:神經網路和 TensorFlow 的文本分類
使用 MPI for Python 並行化遺傳演算法
我是這樣挑戰不用 for 循環的,15篇 Python 技術熱文
程序員必知的 Python 陷阱與缺陷列表

TAG:Python開發者 |

您可能感興趣

VMware workstation的性能優化
億級 Elasticsearch 性能優化
Android 性能優化之內存優化
Android APP 性能優化的一些思考
Nature Communications:金納米團簇光學穩定性調控——增強光氧化還原催化性能
詳解Linux性能調優之tuned特性
Unity Labs:AutoLOD自動化性能提升的實驗
iPhone X Plus基準測試 性能大幅超越Android產品
iPhone 防水性能再強化?蘋果專利曝光 Lightning 傳輸介面新功能
蘇寧Nodejs性能優化實戰
iPhone 7 Plus和iPhone X,性能區別?
Python3 vs. Python2 大作戰,誰將是性能之王?
Python3 vs.Python2 大作戰,誰將是性能之王?
Netty-整合Protobuf高性能數據傳輸
性能測試軟體Loadrunner
linux性能調試之iostat
Intel Hades Canyon NUC評測:令人讚不絕口的性能與拓展能力
Nike Epic React Flyknit 性能分析與搭配
Galaxy S9樹立Android性能新標杆 但iPhone X仍然比它快
利用Skywalking-netcore監控你的應用性能