作者:chen_h
微信號 & QQ:862251340
微信公眾號:coderpai
當(dāng)你嫌棄 Python 速度慢時(shí)
Python編程語言幾乎可用于任何類型的快速原型設(shè)計(jì)和快速開發(fā)。它具有很強(qiáng)的功能,例如它的高級特性,具有幾乎人性化可讀性的語法。此外,它是跨平臺的,具有多樣性的標(biāo)準(zhǔn)庫,它是多范式的,為程序員提供了很多自由,可以使用不同的編程范例,如面向?qū)ο螅δ芑蛘叱绦?。但是,有時(shí)我們系統(tǒng)的某些部分具有高性能要求,因此 Python 提供的速度可能遠(yuǎn)遠(yuǎn)不夠,那么,我們?nèi)绾卧诓浑x開 Python 領(lǐng)域的情況下提高性能。
其中一個(gè)可能的解決方案是使用 Numba,這是一種將 Python 代碼轉(zhuǎn)換為機(jī)器指令的運(yùn)行編輯器,同時(shí)讓我們使用 Python 的簡潔和表達(dá)能力,并實(shí)現(xiàn)機(jī)器碼的速度。
什么是 Numba?
Numba 是一個(gè)執(zhí)行 JIT 編譯的庫,即使用 LLVM 行業(yè)標(biāo)準(zhǔn)編譯器在運(yùn)行時(shí)將純 Python 代碼轉(zhuǎn)換為優(yōu)化的機(jī)器代碼。它還能夠自動(dòng)并行化在多個(gè)核上面運(yùn)行代碼。Numba 是跨平臺的,因?yàn)樗m用于不同的操作系統(tǒng)(Linux,Windows,OSX)和不同的架構(gòu)(x86,x86_64,ppc64le等等)。它還能夠在GPU(NVIDIA CUDA 或者 AMD ROC)上運(yùn)行相同的代碼,并與 Python 2.7 和 3.4-3.7兼容??偟膩碚f,最令人印象深刻的功能是它的使用簡單,因?yàn)槲覀冎恍枰恍┭b飾器來充分利用 JIT 的全部功能。
Numba模式和 @jit 裝飾器
最重要的指令是 @jit 裝飾器。正是這個(gè)裝飾器指示編譯器運(yùn)行哪種模式以及使用什么配置。在這種配置下,我們的裝飾函數(shù)的生成字節(jié)碼與我們在裝飾器中指定的參數(shù)(例如輸入?yún)?shù)的類型)相結(jié)合,進(jìn)行分析,優(yōu)化,最后使用 LLVM 進(jìn)行編譯,生成特定定制的本機(jī)機(jī)器指令。然后,為每個(gè)函數(shù)調(diào)用此編譯版本。
有兩種重要的模式:nopython和object。noPython 完全避免了 Python 解釋器,并將完整代碼轉(zhuǎn)換為可以在沒有 Python 幫助的情況下運(yùn)行的本機(jī)指令。但是,如果由于某種原因,該模式不可用(例如,當(dāng)使用不受支持的 Python功能或者外部庫時(shí)),編譯將回退到對象模式,當(dāng)它無法編譯某些代碼時(shí),它將使用 Python 解釋器。當(dāng)然,nopython 模式是提供最佳性能提升的模式。
Numba 的高層架構(gòu)
Numba 的轉(zhuǎn)換過程可以在一系列重要步驟中進(jìn)行轉(zhuǎn)換,從字節(jié)碼分析到最終機(jī)器代碼生成。下面的圖片說明了這個(gè)過程,其中綠色框?qū)?yīng)于 Numba 編譯器的前端,藍(lán)色框?qū)儆诤蠖恕?
Numba 編譯器首先對所需函數(shù)的 byecode 進(jìn)行分析。此步驟生成一個(gè)描述可能的執(zhí)行流程的圖表,稱為控制流程圖(CFG)?;谠搱D,我們就可以計(jì)算分析整個(gè)過程。完成這些步驟后,編譯器開始將字節(jié)碼轉(zhuǎn)換為中間表示(IR),Numba 將執(zhí)行進(jìn)一步的優(yōu)化和轉(zhuǎn)換。然后,執(zhí)行類型推斷,這是最重要的步驟之一。在此步驟中,編譯器將嘗試推斷所有變量的類型。此外,如果啟用并行設(shè)置,IR 代碼將轉(zhuǎn)換為等效的并行版本。
如果成功推斷出所有類型,則將 Numba IR代碼轉(zhuǎn)換為有效的 LLVM IR 代碼。但是,如果類型推斷過程失敗,LLVM 生成的代碼將會(huì)變慢,因?yàn)樗匀恍枰幚韺?Python C API 的調(diào)用。最后,LLVM IR 代碼由 LLVM JIT 編譯器編譯為本機(jī)指令。然后將這個(gè)優(yōu)化的加工代碼加載到內(nèi)存中,并在對同一函數(shù)的多次調(diào)用中重用,使其比純 Python 快數(shù)百倍。
出于調(diào)試目的,Numba 還提供了一組可以啟用的標(biāo)志,以便查看不同階段的輸出。
os
.
environ
[
"NUMBA_DUMP_CFG"
]
=
"1"
os
.
environ
[
"NUMBA_DUMP_IR"
]
=
"1"
os
.
environ
[
"NUMBA_DUMP_ANNOTATION"
]
=
"1"
os
.
environ
[
"NUMBA_DEBUG_ARRAY_OPT_STATS"
]
=
"1"
os
.
environ
[
"NUMBA_DUMP_LLVM"
]
=
"1"
os
.
environ
[
"NUMBA_DUMP_OPTIMIZED"
]
=
"1"
os
.
environ
[
"NUMBA_DUMP_ASSEMBLY"
]
=
"1"
加速運(yùn)算的一個(gè)例子
我們可以使用 Numba 庫的一個(gè)絕佳例子是進(jìn)行密集的數(shù)值運(yùn)算。舉個(gè)例子,讓我們計(jì)算一組 2 16 2^{16} 2 1 6 個(gè)隨機(jī)數(shù)的 softmax 函數(shù)。softmax 函數(shù),用于將一組實(shí)際值轉(zhuǎn)換為概率并通常用作神經(jīng)網(wǎng)絡(luò)體系結(jié)構(gòu)中的最后一層,定義為:
下面的代碼顯示了這個(gè)函數(shù)的兩個(gè)不同的實(shí)現(xiàn),一個(gè)純 Python 方法,一個(gè)使用 numba 和 numpy 的優(yōu)化版本:
import
time
import
math
import
numpy
as
np
from
numba
import
jit
@jit
(
"f8(f8[:])"
,
cache
=
False
,
nopython
=
True
,
nogil
=
True
,
parallel
=
True
)
def
esum
(
z
)
:
return
np
.
sum
(
np
.
exp
(
z
)
)
@jit
(
"f8[:](f8[:])"
,
cache
=
False
,
nopython
=
True
,
nogil
=
True
,
parallel
=
True
)
def
softmax_optimized
(
z
)
:
num
=
np
.
exp
(
z
)
s
=
num
/
esum
(
z
)
return
s
def
softmax_python
(
z
)
:
s
=
[
]
exp_sum
=
0
for
i
in
range
(
len
(
z
)
)
:
exp_sum
+=
math
.
exp
(
z
[
i
]
)
for
i
in
range
(
len
(
z
)
)
:
s
+=
[
math
.
exp
(
z
[
i
]
)
/
exp_sum
]
return
s
def
main
(
)
:
np
.
random
.
seed
(
0
)
z
=
np
.
random
.
uniform
(
0
,
10
,
10
**
8
)
# generate random floats in the range [0,10)
start
=
time
.
time
(
)
softmax_python
(
z
.
tolist
(
)
)
# run pure python version of softmax
elapsed
=
time
.
time
(
)
-
start
print
(
'Ran pure python softmax calculations in {} seconds'
.
format
(
elapsed
)
)
softmax_optimized
(
z
)
# cache jit compilation
start
=
time
.
time
(
)
softmax_optimized
(
z
)
# run optimzed version of softmax
elapsed
=
time
.
time
(
)
-
start
print
(
'\nRan optimized softmax calculations in {} seconds'
.
format
(
elapsed
)
)
if
__name__
==
'__main__'
:
main
(
)
上述腳本的輸出結(jié)果為:
Ran pure python softmax calculations
in
77.56219696998596
seconds
Ran optimized softmax calculations
in
1.517017126083374
seconds
這些結(jié)果清楚的顯示了將代碼轉(zhuǎn)換為 Numba 能夠理解的代碼時(shí)獲得的性能提升。
在 softmax_optimized 函數(shù)中,已經(jīng)存在 Numba 注釋,它充分利用了 JIT 優(yōu)化的全部功能。事實(shí)上,在編譯過程中,以下字節(jié)碼將被分析,優(yōu)化并編譯為本機(jī)指令:
>
python
import
dis
from
softmax
import
esum
,
softmax_optimized
>>
>
dis
.
dis
(
softmax_optimized
)
14
0
LOAD_GLOBAL
0
(
np
)
2
LOAD_ATTR
1
(
exp
)
4
LOAD_FAST
0
(
z
)
6
CALL_FUNCTION
1
8
STORE_FAST
1
(
num
)
15
10
LOAD_FAST
1
(
num
)
12
LOAD_GLOBAL
2
(
esum
)
14
LOAD_FAST
0
(
z
)
16
CALL_FUNCTION
1
18
BINARY_TRUE_DIVIDE
20
STORE_FAST
2
(
s
)
16
22
LOAD_FAST
2
(
s
)
24
RETURN_VALUE
>>
>
dis
.
dis
(
esum
)
9
0
LOAD_GLOBAL
0
(
np
)
2
LOAD_ATTR
1
(
sum
)
4
LOAD_GLOBAL
0
(
np
)
6
LOAD_ATTR
2
(
exp
)
8
LOAD_FAST
0
(
z
)
10
CALL_FUNCTION
1
12
CALL_FUNCTION
1
14
RETURN_VALUE
我們可以通過簽名提供有關(guān)預(yù)期輸入和輸出類型的更多信息。在上面的示例中,簽名"f8[:](f8[:])" 用于指定函數(shù)接受雙精度浮點(diǎn)數(shù)組并返回另一個(gè) 64 位浮點(diǎn)數(shù)組。
簽名也可以使用顯式類型名稱:“float64 [:](float64 [:])”。一般形式是類型(類型,類型,…),類似于經(jīng)典函數(shù),其中參數(shù)名稱由其類型替換,函數(shù)名稱由其返回類型替換。Numba 接受許多不同的類型,如下所述:
Type name(s) | Type short name | Description |
---|---|---|
boolean | b1 | represented as a byte |
uint8, byte | u1 | 8-bit unsigned byte |
uint16 | u2 | 16-bit unsigned integer |
uint32 | u4 | 32-bit unsigned integer |
uint64 | u8 | 64-bit unsigned integer |
int8, char | i1 | 8-bit signed byte |
int16 | i2 | 16-bit signed integer |
int32 | i4 | 32-bit signed integer |
int64 | i8 | 64-bit signed integer |
intc | – | C |
uintc | – | C |
intp | – | pointer-sized integer |
uintp | – | pointer-sized unsigned integer |
float32 | f4 | single-precision floating-point number |
float64, double | f8 | double-precision floating-point number |
complex64 | c8 | single-precision complex number |
complex128 | c16 | double-precision complex number |
這些注釋很容易使用 [:],[: , :] 或者 [: , : , ;] 分別擴(kuò)展為數(shù)組形式,分別用于 1,2和3維。
最后
Python 是一個(gè)非常棒的工具。但是,它也有一些限制,但是我們可以通過一些別的途徑來提高它的性能,本文介紹的 Nubma 就是一種非常好的方式。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
