作者:chen_h
微信號 & QQ:862251340
微信公眾號:coderpai
當你嫌棄 Python 速度慢時
Python編程語言幾乎可用于任何類型的快速原型設計和快速開發。它具有很強的功能,例如它的高級特性,具有幾乎人性化可讀性的語法。此外,它是跨平臺的,具有多樣性的標準庫,它是多范式的,為程序員提供了很多自由,可以使用不同的編程范例,如面向對象,功能或者程序。但是,有時我們系統的某些部分具有高性能要求,因此 Python 提供的速度可能遠遠不夠,那么,我們如何在不離開 Python 領域的情況下提高性能。
其中一個可能的解決方案是使用 Numba,這是一種將 Python 代碼轉換為機器指令的運行編輯器,同時讓我們使用 Python 的簡潔和表達能力,并實現機器碼的速度。
什么是 Numba?
Numba 是一個執行 JIT 編譯的庫,即使用 LLVM 行業標準編譯器在運行時將純 Python 代碼轉換為優化的機器代碼。它還能夠自動并行化在多個核上面運行代碼。Numba 是跨平臺的,因為它適用于不同的操作系統(Linux,Windows,OSX)和不同的架構(x86,x86_64,ppc64le等等)。它還能夠在GPU(NVIDIA CUDA 或者 AMD ROC)上運行相同的代碼,并與 Python 2.7 和 3.4-3.7兼容??偟膩碚f,最令人印象深刻的功能是它的使用簡單,因為我們只需要一些裝飾器來充分利用 JIT 的全部功能。
Numba模式和 @jit 裝飾器
最重要的指令是 @jit 裝飾器。正是這個裝飾器指示編譯器運行哪種模式以及使用什么配置。在這種配置下,我們的裝飾函數的生成字節碼與我們在裝飾器中指定的參數(例如輸入參數的類型)相結合,進行分析,優化,最后使用 LLVM 進行編譯,生成特定定制的本機機器指令。然后,為每個函數調用此編譯版本。
有兩種重要的模式:nopython和object。noPython 完全避免了 Python 解釋器,并將完整代碼轉換為可以在沒有 Python 幫助的情況下運行的本機指令。但是,如果由于某種原因,該模式不可用(例如,當使用不受支持的 Python功能或者外部庫時),編譯將回退到對象模式,當它無法編譯某些代碼時,它將使用 Python 解釋器。當然,nopython 模式是提供最佳性能提升的模式。
Numba 的高層架構
Numba 的轉換過程可以在一系列重要步驟中進行轉換,從字節碼分析到最終機器代碼生成。下面的圖片說明了這個過程,其中綠色框對應于 Numba 編譯器的前端,藍色框屬于后端。
Numba 編譯器首先對所需函數的 byecode 進行分析。此步驟生成一個描述可能的執行流程的圖表,稱為控制流程圖(CFG)?;谠搱D,我們就可以計算分析整個過程。完成這些步驟后,編譯器開始將字節碼轉換為中間表示(IR),Numba 將執行進一步的優化和轉換。然后,執行類型推斷,這是最重要的步驟之一。在此步驟中,編譯器將嘗試推斷所有變量的類型。此外,如果啟用并行設置,IR 代碼將轉換為等效的并行版本。
如果成功推斷出所有類型,則將 Numba IR代碼轉換為有效的 LLVM IR 代碼。但是,如果類型推斷過程失敗,LLVM 生成的代碼將會變慢,因為它仍然需要處理對 Python C API 的調用。最后,LLVM IR 代碼由 LLVM JIT 編譯器編譯為本機指令。然后將這個優化的加工代碼加載到內存中,并在對同一函數的多次調用中重用,使其比純 Python 快數百倍。
出于調試目的,Numba 還提供了一組可以啟用的標志,以便查看不同階段的輸出。
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"
加速運算的一個例子
我們可以使用 Numba 庫的一個絕佳例子是進行密集的數值運算。舉個例子,讓我們計算一組 2 16 2^{16} 2 1 6 個隨機數的 softmax 函數。softmax 函數,用于將一組實際值轉換為概率并通常用作神經網絡體系結構中的最后一層,定義為:
下面的代碼顯示了這個函數的兩個不同的實現,一個純 Python 方法,一個使用 numba 和 numpy 的優化版本:
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
(
)
上述腳本的輸出結果為:
Ran pure python softmax calculations
in
77.56219696998596
seconds
Ran optimized softmax calculations
in
1.517017126083374
seconds
這些結果清楚的顯示了將代碼轉換為 Numba 能夠理解的代碼時獲得的性能提升。
在 softmax_optimized 函數中,已經存在 Numba 注釋,它充分利用了 JIT 優化的全部功能。事實上,在編譯過程中,以下字節碼將被分析,優化并編譯為本機指令:
>
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
我們可以通過簽名提供有關預期輸入和輸出類型的更多信息。在上面的示例中,簽名"f8[:](f8[:])" 用于指定函數接受雙精度浮點數組并返回另一個 64 位浮點數組。
簽名也可以使用顯式類型名稱:“float64 [:](float64 [:])”。一般形式是類型(類型,類型,…),類似于經典函數,其中參數名稱由其類型替換,函數名稱由其返回類型替換。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 |
這些注釋很容易使用 [:],[: , :] 或者 [: , : , ;] 分別擴展為數組形式,分別用于 1,2和3維。
最后
Python 是一個非常棒的工具。但是,它也有一些限制,但是我們可以通過一些別的途徑來提高它的性能,本文介紹的 Nubma 就是一種非常好的方式。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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