# 7.2。 Numba 建筑
> 原文: [http://numba.pydata.org/numba-doc/latest/developer/architecture.html](http://numba.pydata.org/numba-doc/latest/developer/architecture.html)
## 7.2.1。簡介
Numba 是 Python 字節碼的編譯器,具有可選的類型特化。
假設您在標準 Python 解釋器中輸入這樣的函數(以前稱為“CPython”):
```py
def add(a, b):
return a + b
```
解釋器將立即解析該函數??并將其轉換為字節碼表示,該表示描述 CPython 解釋器應如何在低級別執行該函數。對于上面的示例,它看起來像這樣:
```py
>>> import dis
>>> dis.dis(add)
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_ADD
7 RETURN_VALUE
```
CPython 使用基于堆棧的解釋器(很像 HP 計算器),因此代碼首先將兩個局部變量壓入堆棧。 `BINARY_ADD`操作碼從堆棧中彈出前兩個參數,并進行 Python C API 函數調用,相當于調用`a.__add__(b)`。然后將結果推送到解釋器堆棧的頂部。最后,`RETURN_VALUE`操作碼作為函數調用的結果返回堆棧頂部的值。
Numba 可以使用此字節碼并將其編譯為執行與 CPython 解釋器相同操作的機器代碼,將`a`和`b`視為通用 Python 對象。保留了 Python 的完整語義,并且編譯的函數可以與任何具有 add 運算符定義的對象一起使用。當以這種方式編譯 Numba 函數時,我們說它已經在[對象模式](../glossary.html#term-object-mode)中編譯,因為代碼仍然操縱 Python 對象。
在對象模式下編譯的 Numba 代碼并不比在 CPython 解釋器中執行原始 Python 函數快得多。但是,如果我們將函數專門化為僅使用某些數據類型運行,Numba 可以生成更短,更高效的代碼,本機操作數據而無需調用 Python C API。當為特定數據類型編譯代碼以使函數體不再依賴于 Python 運行時時,我們說該函數已經在 [nopython 模式](../glossary.html#term-nopython-mode)中編譯。以 nopython 模式編譯的數字代碼比原始 Python 快數百倍。
## 7.2.2。編譯器架構
像許多編譯器一樣,Numba 在概念上可以分為 _ 前端 _ 和 _ 后端 _。
Numba _ 前端 _ 包括分析 Python 字節碼的階段,將其轉換為 [Numba IR](../glossary.html#term-numba-ir) 并在 IR 上執行各種轉換和分析步驟。其中一個關鍵步驟是[類型推斷](../glossary.html#term-type-inference)。前端必須成功地明確鍵入所有變量,以便后端在 [nopython 模式](../glossary.html#term-nopython-mode)中生成代碼,因為后端使用類型信息來匹配適當的代碼生成器及其操作的值。
Numba _ 后端 _ 遍歷由前端分析產生的 Numba IR,并利用類型推斷階段推導出的類型信息,為每個遇到的操作生成正確的 LLVM 代碼。生成 LLVM 代碼后,會要求 LLVM 庫對其進行優化,并為最終的本機函數生成本機處理器代碼。
除了編譯器前端和后端之外還有其他部分,例如 JIT 函數的緩存機制。本文檔不考慮這些部分。
## 7.2.3。上下文
Numba 非常靈活,允許它為 CPU 和 GPU 等不同的硬件架構生成代碼。為了支持這些不同的應用,Numba 使用 _ 輸入上下文 _ 和 _ 目標上下文 _。
在編譯器前端中使用 _ 類型上下文 _ 來對函數中的操作和值執行類型推斷。類似的輸入上下文可用于許多體系結構,因為幾乎所有情況下,鍵入推斷都是與硬件無關的。但是,Numba 目前為每個目標都有不同的輸入上下文。
_ 目標上下文 _ 用于生成對類型推斷期間識別的 Numba 類型進行操作所需的特定指令序列。目標上下文是特定于體系結構的,并且在定義執行模型和可用的 Python API 時非常靈活。例如,Numba 為這兩種架構提供了“cpu”和“cuda”上下文,以及產生多線程 CPU 代碼的“并行”上下文。
## 7.2.4。編譯階段
Numba 中的 [`jit()`](../reference/jit-compilation.html#numba.jit "numba.jit") 裝飾器最終調用`numba.compiler.compile_extra()`,它在多階段過程中編譯 Python 函數,如下所述。
### 7.2.4.1。第 1 階段:分析字節碼
在編譯開始時,函數字節碼被傳遞給 Numba 解釋器的實例(`numba.interpreter`)。解釋器對象分析字節碼以找到控制流圖(`numba.controlflow`)。控制流圖(CFG)描述了由于循環和分支,執行可以在函數內從一個塊移動到下一個塊的方式。
數據流分析(`numba.dataflow`)獲取控制流圖,并跟蹤如何從不同代碼路徑的 Python 解釋器堆棧中推送和彈出值。這對于理解第 2 階段所需的堆棧變量的生命周期非常重要。
如果將環境變量`NUMBA_DUMP_CFG`設置為 1,Numba 會將控制流圖分析的結果轉儲到屏幕上。我們的`add()`示例非常無聊,因為只有一個語句塊:
```py
CFG adjacency lists:
{0: []}
CFG dominators:
{0: set([0])}
CFG post-dominators:
{0: set([0])}
CFG back edges: []
CFG loops:
{}
CFG node-to-loops:
{0: []}
```
具有更復雜流量控制的功能將具有更有趣的控制流程圖。這個功能:
```py
def doloops(n):
acc = 0
for i in range(n):
acc += 1
if n == 10:
break
return acc
```
編譯到這個字節碼:
```py
9 0 LOAD_CONST 1 (0)
3 STORE_FAST 1 (acc)
10 6 SETUP_LOOP 46 (to 55)
9 LOAD_GLOBAL 0 (range)
12 LOAD_FAST 0 (n)
15 CALL_FUNCTION 1
18 GET_ITER
>> 19 FOR_ITER 32 (to 54)
22 STORE_FAST 2 (i)
11 25 LOAD_FAST 1 (acc)
28 LOAD_CONST 2 (1)
31 INPLACE_ADD
32 STORE_FAST 1 (acc)
12 35 LOAD_FAST 0 (n)
38 LOAD_CONST 3 (10)
41 COMPARE_OP 2 (==)
44 POP_JUMP_IF_FALSE 19
13 47 BREAK_LOOP
48 JUMP_ABSOLUTE 19
51 JUMP_ABSOLUTE 19
>> 54 POP_BLOCK
14 >> 55 LOAD_FAST 1 (acc)
58 RETURN_VALUE
```
該字節碼的相應 CFG 是:
```py
CFG adjacency lists:
{0: [6], 6: [19], 19: [54, 22], 22: [19, 47], 47: [55], 54: [55], 55: []}
CFG dominators:
{0: set([0]),
6: set([0, 6]),
19: set([0, 6, 19]),
22: set([0, 6, 19, 22]),
47: set([0, 6, 19, 22, 47]),
54: set([0, 6, 19, 54]),
55: set([0, 6, 19, 55])}
CFG post-dominators:
{0: set([0, 6, 19, 55]),
6: set([6, 19, 55]),
19: set([19, 55]),
22: set([22, 55]),
47: set([47, 55]),
54: set([54, 55]),
55: set([55])}
CFG back edges: [(22, 19)]
CFG loops:
{19: Loop(entries=set([6]), exits=set([54, 47]), header=19, body=set([19, 22]))}
CFG node-to-loops:
{0: [], 6: [], 19: [19], 22: [19], 47: [], 54: [], 55: []}
```
CFG 中的數字指的是上面操作碼名稱左側顯示的字節碼偏移量。
### 7.2.4.2。第 2 階段:生成 Numba IR
一旦控制流和數據分析完成,Numba 解釋器就可以逐步執行字節碼并將其轉換為 Numba 內部中間表示。此轉換過程將函數從堆棧計算機表示(由 Python 解釋器使用)更改為寄存器計算機表示(由 LLVM 使用)。
盡管 IR 作為對象樹存儲在內存中,但它可以序列化為字符串以進行調試。如果將環境變量`NUMBA_DUMP_IR`設置為 1,則 Numba IR 將被轉儲到屏幕上。對于上述`add()`功能,Numba IR 看起來像:
```py
label 0:
a = arg(0, name=a) ['a']
b = arg(1, name=b) ['b']
$0.3 = a + b ['$0.3', 'a', 'b']
del b []
del a []
$0.4 = cast(value=$0.3) ['$0.3', '$0.4']
del $0.3 []
return $0.4 ['$0.4']
```
`del`指令由[實時變量分析](live_variable_analysis.html#live-variable-analysis)生成。這些說明可確保參考不會泄露。在 [nopython 模式](../glossary.html#term-nopython-mode)中,一些對象由 numba 運行時跟蹤,而另一些則不是。對于跟蹤對象,發出取消引用操作;否則,該指令是無操作。在[對象模式](../glossary.html#term-object-mode)中,每個變量都包含對 PyObject 的擁有引用。
### 7.2.4.3。第 3 階段:宏觀擴張
現在該函數已被轉換為 Numba IR,可以執行宏擴展。宏擴展將 Numba 已知的特定屬性轉換為表示函數調用的 IR 節點。這在`numba.compiler.translate_stage`功能中啟動,并在`numba.macro`中實現。
宏擴展的屬性示例包括網格,塊和線程維度和索引的 CUDA 內在函數。例如,在以下函數中分配給`tx`:
```py
@cuda.jit(argtypes=[f4[:]])
def f(a):
tx = cuda.threadIdx.x
```
在翻譯成 Numba IR 之后有以下代表:
```py
$0.1 = global(cuda: <module 'numba.cuda' from '...'>) ['$0.1']
$0.2 = getattr(value=$0.1, attr=threadIdx) ['$0.1', '$0.2']
del $0.1 []
$0.3 = getattr(value=$0.2, attr=x) ['$0.2', '$0.3']
del $0.2 []
tx = $0.3 ['$0.3', 'tx']
```
宏擴展后,`$0.3 = getattr(value=$0.2, attr=x)` IR 節點轉換為:
```py
$0.3 = call tid.x(, ) ['$0.3']
```
它表示用于調用`tid.x`內部函數的`Intrinsic` IR 節點的實例。
### 7.2.4.4。第 4 階段:重寫無類型 IR
在運行類型推斷之前,可能需要在 Numba IR 上運行某些轉換。一個這樣的例子是檢測具有隱式常量參數的`raise`語句,以便在 [nopython 模式](../glossary.html#term-nopython-mode)中支持它們。假設您使用 Numba 編譯以下函數:
```py
def f(x):
if x == 0:
raise ValueError("x cannot be zero")
```
如果將 [`NUMBA_DUMP_IR`](../reference/envvars.html#envvar-NUMBA_DUMP_IR) 環境變量設置為`1`,您將看到在類型推斷階段之前重寫 IR:
```py
REWRITING:
del $0.3 []
$12.1 = global(ValueError: <class 'ValueError'>) ['$12.1']
$const12.2 = const(str, x cannot be zero) ['$const12.2']
$12.3 = call $12.1($const12.2) ['$12.1', '$12.3', '$const12.2']
del $const12.2 []
del $12.1 []
raise $12.3 ['$12.3']
____________________________________________________________
del $0.3 []
$12.1 = global(ValueError: <class 'ValueError'>) ['$12.1']
$const12.2 = const(str, x cannot be zero) ['$const12.2']
$12.3 = call $12.1($const12.2) ['$12.1', '$12.3', '$const12.2']
del $const12.2 []
del $12.1 []
raise <class 'ValueError'>('x cannot be zero') []
```
### 7.2.4.5。第 5 階段:推斷類型
現在已經生成了 Numba IR 并進行了宏擴展,可以執行類型分析。函數參數的類型可以從`@jit`裝飾器中給出的顯式函數簽名(例如`@jit('float64(float64, float64)')`)獲取,或者如果在函數發生編譯時可以從實際函數參數的類型中獲取它們首先被稱為。
類型推理引擎可在`numba.typeinfer`中找到。它的工作是為 Numba IR 中的每個中間變量分配一個類型。通過將 [`NUMBA_DUMP_ANNOTATION`](../reference/envvars.html#envvar-NUMBA_DUMP_ANNOTATION) 環境變量設置為 1 可以看到此過程的結果:
```py
-----------------------------------ANNOTATION-----------------------------------
# File: archex.py
# --- LINE 4 ---
@jit(nopython=True)
# --- LINE 5 ---
def add(a, b):
# --- LINE 6 ---
# label 0
# a = arg(0, name=a) :: int64
# b = arg(1, name=b) :: int64
# $0.3 = a + b :: int64
# del b
# del a
# $0.4 = cast(value=$0.3) :: int64
# del $0.3
# return $0.4
return a + b
```
如果類型推斷無法為所有中間變量找到一致的類型賦值,它會將每個變量標記為類型`pyobject`并回退到對象模式。在函數體中使用不受支持的 Python 類型,語言功能或函數時,類型推斷可能會失敗。
### 7.2.4.6。階段 6a:重寫類型 IR
此過程的目的是執行仍然需要或至少可以從 Numba IR 類型信息中受益的任何高級優化。
一旦降低就不容易優化的問題域的一個示例是多維陣列操作的域。當 Numba 降低數組操作時,Numba 將操作視為完整的 ufunc 內核。在降低單個陣列操作期間,Numba 生成一個內聯廣播循環,用于創建新的結果數組。然后 Numba 生成一個應用程序循環,將運算符應用于數組輸入。一旦將這些循環降低到 LLVM 中,識別并重寫這些循環即使不是不可能,也很難。
數組運算符域中的一對示例優化是循環融合和快捷方式砍伐森林。當優化器識別出一個數組運算符的輸出正被送入另一個數組運算符,并且只被送入該數組運算符時,它可以將兩個循環融合到一個循環中。優化器可以通過直接將第一操作的結果饋送到第二操作,跳過存儲并加載到中間陣列來進一步消除為初始操作分配的臨時陣列。這種消除被稱為捷徑砍伐森林。 Numba 目前使用重寫傳遞來實現這些數組優化。有關詳細信息,請參閱本文檔后面的“[案例研究:數組表達式](rewrites.html#case-study-array-expressions)”小節。
通過將 [`NUMBA_DUMP_IR`](../reference/envvars.html#envvar-NUMBA_DUMP_IR) 環境變量設置為非零值(例如 1),可以看到重寫的結果。以下示例顯示了重寫過程的輸出,因為它識別由乘法和加法組成的數組表達式,并輸出融合內核作為特殊運算符,`arrayexpr()`:
```py
______________________________________________________________________
REWRITING:
a0 = arg(0, name=a0) ['a0']
a1 = arg(1, name=a1) ['a1']
a2 = arg(2, name=a2) ['a2']
$0.3 = a0 * a1 ['$0.3', 'a0', 'a1']
del a1 []
del a0 []
$0.5 = $0.3 + a2 ['$0.3', '$0.5', 'a2']
del a2 []
del $0.3 []
$0.6 = cast(value=$0.5) ['$0.5', '$0.6']
del $0.5 []
return $0.6 ['$0.6']
____________________________________________________________
a0 = arg(0, name=a0) ['a0']
a1 = arg(1, name=a1) ['a1']
a2 = arg(2, name=a2) ['a2']
$0.5 = arrayexpr(ty=array(float64, 1d, C), expr=('+', [('*', [Var(a0, test.py (14)), Var(a1, test.py (14))]), Var(a2, test.py (14))])) ['$0.5', 'a0', 'a1', 'a2']
del a0 []
del a1 []
del a2 []
$0.6 = cast(value=$0.5) ['$0.5', '$0.6']
del $0.5 []
return $0.6 ['$0.6']
______________________________________________________________________
```
在重寫之后,Numba 將數組表達式降低為一個新的類似 ufunc 的函數,該函數內聯到一個僅分配單個結果數組的循環中。
### 7.2.4.7。階段 6b:執行自動并行化
僅當 [`jit()`](../reference/jit-compilation.html#numba.jit "numba.jit") 裝飾器中的`parallel`選項設置為`True`時,才會執行此過程。此過程在 Numba IR 中的操作語義中隱含發現并行性,并使用特殊的 <cite>parfor</cite> 運算符替換那些操作的顯式并行表示。然后,執行優化以最大化彼此相鄰的 parfors 的數量,使得它們可以融合在一起,只需要一次通過數據,因此通常具有更好的高速緩存性能。最后,在降低期間,這些 parfor 運算符將轉換為類似于 guvectorize 的形式,以實現實際的并行性。
自動并行化傳遞有許多子傳遞,其中許多子控件可以通過 [`jit()`](../reference/jit-compilation.html#numba.jit "numba.jit") 的`parallel`關鍵字參數傳遞的選項字典來控制:
```py
{ 'comprehension': True/False, # parallel comprehension
'prange': True/False, # parallel for-loop
'numpy': True/False, # parallel numpy calls
'reduction': True/False, # parallel reduce calls
'setitem': True/False, # parallel setitem
'stencil': True/False, # parallel stencils
'fusion': True/False, # enable fusion or not
}
```
對于所有這些,默認設置為 <cite>True</cite> 。在以下段落中更詳細地描述了子通道。
1. ```py
CFG Simplification
```
有時 Numba IR 將包含不包含循環的塊鏈,這些循環在此子過程中合并為單個塊。該子通過簡化了 IR 的后續分析。
2. ```py
Numpy canonicalization
```
一些 Numpy 操作可以寫為 Numpy 對象上的操作(例如`arr.sum()`),或者作為 Numpy 對這些對象的調用(例如`numpy.sum(arr)`)。該子過程將所有這些操作轉換為后一種形式,以便進行更清晰的后續分析。
3. ```py
Array analysis
```
后期 parfor 融合的一個關鍵要求是 parfors 具有相同的迭代空間,這些迭代空間通常對應于 Numpy 數組的維度大小。在該子過程中,分析 IR 以確定 Numpy 陣列的維度的等價類。考慮示例`a = b + 1`,其中`a`和`b`都是 Numpy 數組。在這里,我們知道`a`的每個維度必須與`b`的相應維度具有相同的等價類。通常,富含 Numpy 操作的例程將使函數中創建的所有數組都能完全知道等價類。
數組分析還將推斷切片選擇的大小等效性和布爾數組掩蔽(僅一維)。例如,它能夠推斷`a[1 : n-1]`與`b[0 : n-2]`的大小相同。
陣列分析還可以插入安全假設,以確保在并行操作之前滿足與陣列大小相關的前提條件。例如,2-D 矩陣`X`和 1-D 矢量`w`之間的`np.dot(X, w)`要求`X`的第二維與`w`具有相同的尺寸。通常會自動插入這種運行時檢查,但如果數組分析可以推斷出這種等效性,它將跳過它們。
用戶甚至可以通過將關于數組大小的隱式知識轉換為顯式斷言來幫助進行數組分析。例如,在下面的代碼中:
```py
@numba.njit(parallel=True)
def logistic_regression(Y, X, w, iterations):
assert(X.shape == (Y.shape[0], w.shape[0]))
for i in range(iterations):
w -= np.dot(((1.0 / (1.0 + np.exp(-Y * np.dot(X, w))) - 1.0) * Y), X)
return w
```
進行顯式斷言有助于消除函數其余部分中的所有邊界檢查。
4. ```py
prange() to parfor
```
在 for 循環中使用 prange([顯式并行循環](../user/parallel.html#numba-prange))是程序員明確表示 for 循環的所有迭代都可以并行執行。在這個子過程中,我們分析 CFG 以定位循環并將由 prange 對象控制的循環轉換為顯式 <cite>parfor</cite> 運算符。每個顯式 parfor 運算符包括:
1. 循環嵌套信息列表,用于描述 parfor 的迭代空間。循環嵌套列表中的每個條目都包含索引變量,范圍的起點,范圍的結束以及每次迭代的步長值。
2. 初始化(init)塊,包含在 parfor 開始執行之前執行一次的指令。
3. 循環體包括一組基本塊,其對應于循環體并計算迭代空間中的一個點。
4. 用于迭代空間的每個維度的索引變量。
對于 parfor <cite>pranges</cite> ,循環嵌套是一個單獨的條目,其中 start,stop 和 step 字段來自指定的 <cite>prange</cite> 。對于 <cite>prange</cite> parfors,init 塊為空,循環體是循環中的塊集減去循環頭。
通過并行化,數組理解( [List comprehension](../reference/pysupported.html#pysupported-comprehension) )也將被轉換為 prange 以便并行運行。通過設置`parallel={'comprehension': False}`禁用此行為。
同樣,通過設置`parallel={'prange': False}`可以禁用整體 <cite>prange</cite> 到 <cite>parfor</cite> 轉換,在這種情況下, <cite>prange</cite> 的處理方式與<cite>范圍相同</cite> ]。
5. ```py
Numpy to parfor
```
在這個子通道中,Numpy 函數如`ones`,`zeros`,`dot`,大多數隨機數生成函數,arrayexprs(來自 Section [Stage 6a:Rewrite typed IR](#rewrite-typed-ir) )和 Numpy 減少量轉換為 parfors。通常,此轉換會創建循環嵌套列表,其長度等于 IR 中賦值指令左側的維數。左側陣列的尺寸的數量和大小取自上面的子通道 3 中生成的陣列分析信息。創建結果 Numpy 數組的指令生成并存儲在新 parfor 的 init 塊中。為循環體創建基本塊,并生成指令并將其添加到該塊的末尾,以將計算結果存儲到迭代空間中當前點的數組中。存儲到數組中的結果取決于要轉換的操作。例如,對于`ones`,存儲的值是常量 1.對于生成隨機數組的調用,該值來自對相同隨機數函數的調用,但是大小參數被刪除,因此返回標量。對于 arrayexpr 運算符,arrayexpr 樹將轉換為 Numba IR,并且該表達式樹的根處的值用于寫入輸出數組。可以通過設置`parallel={'numpy': False}`禁用從 Numpy 函數和 arrayexpr 運算符到 <cite>parfor</cite> 的轉換。
對于縮減,類似地使用要減少的陣列的陣列分析信息來創建循環嵌套列表。在 init 塊中,初始值被分配給 reduce 變量。循環體由單個塊組成,其中取出迭代空間中的下一個值,并將縮減操作應用于該值,并將當前縮減值和結果存儲回縮減值。通過設置`parallel={'reduction': False}`可以禁用將縮小功能轉換為 <cite>parfor</cite> 。
將 [`NUMBA_DEBUG_ARRAY_OPT_STATS`](../reference/envvars.html#envvar-NUMBA_DEBUG_ARRAY_OPT_STATS) 環境變量設置為 1 將顯示有關 parfor 轉換的一般統計信息。
6. ```py
Setitem to parfor
```
使用切片或布爾數組選擇設置數組元素的范圍也可以并行運行。如果滿足下列條件之一,`A[P] = B[Q]`(或更簡單的情況`A[P] = c`,其中`c`是標量)等語句將轉換為 <cite>parfor</cite> :
> 1. `P`和`Q`是涉及標量和切片的切片或多維選擇器,并且通過陣列分析將`A[P]`和`B[Q]`視為大小等效。僅支持 2 值切片/范圍,帶步長的 3 值不會轉換為 <cite>parfor</cite> 。
> 2. `P`和`Q`是相同的布爾數組。
可以通過設置`parallel={'setitem': False}`禁用此轉換。
7. ```py
Simplification
```
執行復制傳播和死代碼消除傳遞。
8. ```py
Fusion
```
該子通道首先處理每個基本塊并對塊內的指令進行重新排序,目標是在塊中將 parfors 推低,并將非 parfors 提升到塊的開始。在實踐中,這種方法很好地使得在 IR 中彼此相鄰的 parfors,這使得更多的 parfors 可以融合。在 parfor 融合期間,重復掃描每個基本塊直到不可能進一步融合。在此掃描期間,考慮每組相鄰指令。如果符合以下條件,相鄰指令將融合
1. 他們都是 parfors
2. parfors 的循環嵌套具有相同的大小,并且循環嵌套的每個維度的數組等價類是相同的,并且
3. 第一個 parfor 不會創建第二個 parfor 使用的縮減變量。
將兩個 parfors 融合在一起,將第二個 parfor 的 init 塊添加到第一個,將兩個 parfors 的循環體合并在一起,并用第一個 parfor 的循環索引變量替換第二個 parfor 的循環中第二個 parfor 的循環索引變量的實例。可以通過設置`parallel={'fusion': False}`來禁用 Fusion。
將 [`NUMBA_DEBUG_ARRAY_OPT_STATS`](../reference/envvars.html#envvar-NUMBA_DEBUG_ARRAY_OPT_STATS) 環境變量設置為 1 將顯示有關 parfor 融合的一些統計信息。
9. ```py
Push call objects and compute parfor parameters
```
在[階段 7a:生成 nopython LLVM IR](#lowering) 中描述的降低階段中,每個 parfor 成為在`guvectorize`( [@guvectorize 裝飾器](../user/vectorize.html#guvectorize))樣式中并行執行的單獨函數。由于 parfors 可能使用先前在函數中定義的變量,當這些 parfors 成為單獨的函數時,這些變量必須作為參數傳遞給 parfor 函數。在該子通道中,對每個 parfor 主體進行 use-def 掃描,并且活躍度信息用于確定使用但未由 parfor 定義的變量。該變量列表存儲在 parfor 中,以便在降低期間使用。函數變量是此過程中的一個特例,因為函數變量無法傳遞給在 nopython 模式下編譯的函數。相反,對于函數變量,此子過程將賦值指令推送到 parfor 主體中,以便不需要將它們作為參數傳遞。
要查看上述子通道與其他調試信息之間的中間 IR,請將 [`NUMBA_DEBUG_ARRAY_OPT`](../reference/envvars.html#envvar-NUMBA_DEBUG_ARRAY_OPT) 環境變量設置為 1.對于[第 6a 階段:重寫類型 IR](#rewrite-typed-ir) 的示例,在此階段生成以下具有 parfor 的 IR:
```py
______________________________________________________________________
label 0:
a0 = arg(0, name=a0) ['a0']
a0_sh_attr0.0 = getattr(attr=shape, value=a0) ['a0', 'a0_sh_attr0.0']
$consta00.1 = const(int, 0) ['$consta00.1']
a0size0.2 = static_getitem(value=a0_sh_attr0.0, index_var=$consta00.1, index=0) ['$consta00.1', 'a0_sh_attr0.0', 'a0size0.2']
a1 = arg(1, name=a1) ['a1']
a1_sh_attr0.3 = getattr(attr=shape, value=a1) ['a1', 'a1_sh_attr0.3']
$consta10.4 = const(int, 0) ['$consta10.4']
a1size0.5 = static_getitem(value=a1_sh_attr0.3, index_var=$consta10.4, index=0) ['$consta10.4', 'a1_sh_attr0.3', 'a1size0.5']
a2 = arg(2, name=a2) ['a2']
a2_sh_attr0.6 = getattr(attr=shape, value=a2) ['a2', 'a2_sh_attr0.6']
$consta20.7 = const(int, 0) ['$consta20.7']
a2size0.8 = static_getitem(value=a2_sh_attr0.6, index_var=$consta20.7, index=0) ['$consta20.7', 'a2_sh_attr0.6', 'a2size0.8']
---begin parfor 0---
index_var = parfor_index.9
LoopNest(index_variable=parfor_index.9, range=0,a0size0.2,1 correlation=5)
init block:
$np_g_var.10 = global(np: <module 'numpy' from '/usr/local/lib/python3.5/dist-packages/numpy/__init__.py'>) ['$np_g_var.10']
$empty_attr_attr.11 = getattr(attr=empty, value=$np_g_var.10) ['$empty_attr_attr.11', '$np_g_var.10']
$np_typ_var.12 = getattr(attr=float64, value=$np_g_var.10) ['$np_g_var.10', '$np_typ_var.12']
$0.5 = call $empty_attr_attr.11(a0size0.2, $np_typ_var.12, kws=(), func=$empty_attr_attr.11, vararg=None, args=[Var(a0size0.2, test2.py (7)), Var($np_typ_var.12, test2.py (7))]) ['$0.5', '$empty_attr_attr.11', '$np_typ_var.12', 'a0size0.2']
label 1:
$arg_out_var.15 = getitem(value=a0, index=parfor_index.9) ['$arg_out_var.15', 'a0', 'parfor_index.9']
$arg_out_var.16 = getitem(value=a1, index=parfor_index.9) ['$arg_out_var.16', 'a1', 'parfor_index.9']
$arg_out_var.14 = $arg_out_var.15 * $arg_out_var.16 ['$arg_out_var.14', '$arg_out_var.15', '$arg_out_var.16']
$arg_out_var.17 = getitem(value=a2, index=parfor_index.9) ['$arg_out_var.17', 'a2', 'parfor_index.9']
$expr_out_var.13 = $arg_out_var.14 + $arg_out_var.17 ['$arg_out_var.14', '$arg_out_var.17', '$expr_out_var.13']
$0.5[parfor_index.9] = $expr_out_var.13 ['$0.5', '$expr_out_var.13', 'parfor_index.9']
----end parfor 0----
$0.6 = cast(value=$0.5) ['$0.5', '$0.6']
return $0.6 ['$0.6']
______________________________________________________________________
```
### 7.2.4.8。階段 7a:生成 nopython LLVM IR
如果類型推斷成功為每個中間變量找到 Numba 類型,那么 Numba 可以(可能)生成專門的本機代碼。該過程稱為[降低](../glossary.html#term-lowering)。通過使用來自 [llvmlite](http://llvmlite.pydata.org/) 的輔助類,將 Numba IR 樹轉換為 LLVM IR。機器生成的 LLVM IR 看起來不必要地冗長,但 LLVM 工具鏈能夠很容易地將其優化為緊湊,高效的代碼。
基本降低算法是通用的,但特定 Numba IR 節點如何轉換為 LLVM 指令的細節由選擇用于編譯的目標上下文處理。默認目標上下文是在`numba.targets.cpu`中定義的“cpu”上下文。
可以通過將 [`NUMBA_DUMP_LLVM`](../reference/envvars.html#envvar-NUMBA_DUMP_LLVM) 環境變量設置為 1 來顯示 LLVM IR。對于“cpu”上下文,我們的`add()`示例如下所示:
```py
define i32 @"__main__.add$1.int64.int64"(i64* %"retptr",
{i8*, i32}** %"excinfo",
i8* %"env",
i64 %"arg.a", i64 %"arg.b")
{
entry:
%"a" = alloca i64
%"b" = alloca i64
%"$0.3" = alloca i64
%"$0.4" = alloca i64
br label %"B0"
B0:
store i64 %"arg.a", i64* %"a"
store i64 %"arg.b", i64* %"b"
%".8" = load i64* %"a"
%".9" = load i64* %"b"
%".10" = add i64 %".8", %".9"
store i64 %".10", i64* %"$0.3"
%".12" = load i64* %"$0.3"
store i64 %".12", i64* %"$0.4"
%".14" = load i64* %"$0.4"
store i64 %".14", i64* %"retptr"
ret i32 0
}
```
通過將 [`NUMBA_DUMP_OPTIMIZED`](../reference/envvars.html#envvar-NUMBA_DUMP_OPTIMIZED) 設置為 1,可以輸出優化后的 LLVM IR。優化器可以顯著縮短上面生成的代碼:
```py
define i32 @"__main__.add$1.int64.int64"(i64* nocapture %retptr,
{ i8*, i32 }** nocapture readnone %excinfo,
i8* nocapture readnone %env,
i64 %arg.a, i64 %arg.b)
{
entry:
%.10 = add i64 %arg.b, %arg.a
store i64 %.10, i64* %retptr, align 8
ret i32 0
}
```
如果在[階段 6b:執行自動并行化](#parallel-accelerator)期間創建,則以下列方式降低 parfor 操作。首先,使用正常的降低代碼將 parfor 的 init 塊中的指令降低到現有函數中。其次,parfor 的循環體轉變為單獨的 GUFunc。第三,為當前函數發出代碼以調用并行 GUFunc。
要從 parfor 主體創建 GUFunc,GUFunc 的簽名是通過獲取 Stage [Stage 6b 的步驟 9 中確定的 parfor 參數創建的:執行自動并行化](#parallel-accelerator)并添加一個特殊的<cite>時間表</cite>參數,GUFunc 將在其中并行化。 schedule 參數實際上是將 parfor 迭代空間的部分映射到 Numba 線程的靜態調度,因此調度數組的長度與配置的 Numba 線程的數量相同。為了使這個過程更容易并且更少依賴于對 Numba IR 的更改,此階段創建一個 Python 函數作為文本,其中包含 GUFunc 的參數和迭代代碼,該代碼獲取當前調度條目并循環遍歷迭代空間的指定部分。在該循環的主體中,插入一個特殊的標記,以便隨后輕松定位。處理迭代空間處理的代碼然后被`eval`編輯,并且調用 Numba 編譯器的 run_frontend 函數來生成 IR。掃描 IR 以定位哨兵,并用穹頂的環體替換哨兵。然后,通過使用 Numba 編譯器的`compile_ir`函數編譯此合并的 IR 來完成創建并行 GUFunc 的過程。
要調用并行 GUFunc,必須創建靜態調度。插入代碼以調用名為`do_scheduling.`的函數。使用每個 parfor 維度的大小和配置的 Numba 線程的數量 <cite>N</cite> ( [`NUMBA_NUM_THREADS`](../reference/envvars.html#envvar-NUMBA_NUM_THREADS) )調用此函數。 `do_scheduling`函數將迭代空間劃分為 N 個近似相等大小的區域(1D 為線性,2D 為矩形,3 + D 為超矩形),結果時間表傳遞給并行 GUFunc。專用于完整迭代空間的給定維度的線程數大致與給定維度的大小與迭代空間的所有維度的大小之和的比率成比例。
GUFuncs 本身并不提供并行縮減,但降低 parfor 的策略允許我們以可以并行執行縮減的方式使用 GUFunc。為了實現這一點,對于由 parfor 計算的每個縮減變量,并行 GUFunc 和調用它的代碼被修改以使標量縮減變量成為一個縮減變量數組,其長度等于 Numba 線程的數量。此外,GUFunc 仍然包含縮減變量的標量版本,該變量在每次迭代期間由 parfor 正文更新。一次在 GUFunc 結束時,這個局部縮減變量被復制到縮小數組中。以這種方式,防止了減少陣列的錯誤共享。在并行 GUFunc 返回后,代碼也會插入到 main 函數中,該函數在這個較小的縮減數組中進行縮減,然后將此最終縮減值存儲到原始標量縮減變量中。
對應于[階段 6b:執行自動并行化](#parallel-accelerator)中的示例的 GUFunc 可以在下面看到:
```py
______________________________________________________________________
label 0:
sched.29 = arg(0, name=sched) ['sched.29']
a0 = arg(1, name=a0) ['a0']
a1 = arg(2, name=a1) ['a1']
a2 = arg(3, name=a2) ['a2']
_0_5 = arg(4, name=_0_5) ['_0_5']
$3.1.24 = global(range: <class 'range'>) ['$3.1.24']
$const3.3.21 = const(int, 0) ['$const3.3.21']
$3.4.23 = getitem(value=sched.29, index=$const3.3.21) ['$3.4.23', '$const3.3.21', 'sched.29']
$const3.6.28 = const(int, 1) ['$const3.6.28']
$3.7.27 = getitem(value=sched.29, index=$const3.6.28) ['$3.7.27', '$const3.6.28', 'sched.29']
$const3.8.32 = const(int, 1) ['$const3.8.32']
$3.9.31 = $3.7.27 + $const3.8.32 ['$3.7.27', '$3.9.31', '$const3.8.32']
$3.10.36 = call $3.1.24($3.4.23, $3.9.31, kws=[], func=$3.1.24, vararg=None, args=[Var($3.4.23, <string> (2)), Var($3.9.31, <string> (2))]) ['$3.1.24', '$3.10.36', '$3.4.23', '$3.9.31']
$3.11.30 = getiter(value=$3.10.36) ['$3.10.36', '$3.11.30']
jump 1 []
label 1:
$28.2.35 = iternext(value=$3.11.30) ['$28.2.35', '$3.11.30']
$28.3.25 = pair_first(value=$28.2.35) ['$28.2.35', '$28.3.25']
$28.4.40 = pair_second(value=$28.2.35) ['$28.2.35', '$28.4.40']
branch $28.4.40, 2, 3 ['$28.4.40']
label 2:
$arg_out_var.15 = getitem(value=a0, index=$28.3.25) ['$28.3.25', '$arg_out_var.15', 'a0']
$arg_out_var.16 = getitem(value=a1, index=$28.3.25) ['$28.3.25', '$arg_out_var.16', 'a1']
$arg_out_var.14 = $arg_out_var.15 * $arg_out_var.16 ['$arg_out_var.14', '$arg_out_var.15', '$arg_out_var.16']
$arg_out_var.17 = getitem(value=a2, index=$28.3.25) ['$28.3.25', '$arg_out_var.17', 'a2']
$expr_out_var.13 = $arg_out_var.14 + $arg_out_var.17 ['$arg_out_var.14', '$arg_out_var.17', '$expr_out_var.13']
_0_5[$28.3.25] = $expr_out_var.13 ['$28.3.25', '$expr_out_var.13', '_0_5']
jump 1 []
label 3:
$const44.1.33 = const(NoneType, None) ['$const44.1.33']
$44.2.39 = cast(value=$const44.1.33) ['$44.2.39', '$const44.1.33']
return $44.2.39 ['$44.2.39']
______________________________________________________________________
```
### 7.2.4.9。階段 7b:生成對象模式 LLVM IR
如果類型推斷無法為函數內的所有值找到 Numba 類型,則該函數將在對象模式下編譯。生成的 LLVM 將顯著更長,因為編譯的代碼需要調用 [Python C API](https://docs.python.org/3/c-api/) 來執行基本上所有的操作。我們的示例`add()`功能的優化 LLVM 是:
```py
@PyExc_SystemError = external global i8
@".const.Numba_internal_error:_object_mode_function_called_without_an_environment" = internal constant [73 x i8] c"Numba internal error: object mode function called without an environment\00"
@".const.name_'a'_is_not_defined" = internal constant [24 x i8] c"name 'a' is not defined\00"
@PyExc_NameError = external global i8
@".const.name_'b'_is_not_defined" = internal constant [24 x i8] c"name 'b' is not defined\00"
define i32 @"__main__.add$1.pyobject.pyobject"(i8** nocapture %retptr, { i8*, i32 }** nocapture readnone %excinfo, i8* readnone %env, i8* %arg.a, i8* %arg.b) {
entry:
%.6 = icmp eq i8* %env, null
br i1 %.6, label %entry.if, label %entry.endif, !prof !0
entry.if: ; preds = %entry
tail call void @PyErr_SetString(i8* @PyExc_SystemError, i8* getelementptr inbounds ([73 x i8]* @".const.Numba_internal_error:_object_mode_function_called_without_an_environment", i64 0, i64 0))
ret i32 -1
entry.endif: ; preds = %entry
tail call void @Py_IncRef(i8* %arg.a)
tail call void @Py_IncRef(i8* %arg.b)
%.21 = icmp eq i8* %arg.a, null
br i1 %.21, label %B0.if, label %B0.endif, !prof !0
B0.if: ; preds = %entry.endif
tail call void @PyErr_SetString(i8* @PyExc_NameError, i8* getelementptr inbounds ([24 x i8]* @".const.name_'a'_is_not_defined", i64 0, i64 0))
tail call void @Py_DecRef(i8* null)
tail call void @Py_DecRef(i8* %arg.b)
ret i32 -1
B0.endif: ; preds = %entry.endif
%.30 = icmp eq i8* %arg.b, null
br i1 %.30, label %B0.endif1, label %B0.endif1.1, !prof !0
B0.endif1: ; preds = %B0.endif
tail call void @PyErr_SetString(i8* @PyExc_NameError, i8* getelementptr inbounds ([24 x i8]* @".const.name_'b'_is_not_defined", i64 0, i64 0))
tail call void @Py_DecRef(i8* %arg.a)
tail call void @Py_DecRef(i8* null)
ret i32 -1
B0.endif1.1: ; preds = %B0.endif
%.38 = tail call i8* @PyNumber_Add(i8* %arg.a, i8* %arg.b)
%.39 = icmp eq i8* %.38, null
br i1 %.39, label %B0.endif1.1.if, label %B0.endif1.1.endif, !prof !0
B0.endif1.1.if: ; preds = %B0.endif1.1
tail call void @Py_DecRef(i8* %arg.a)
tail call void @Py_DecRef(i8* %arg.b)
ret i32 -1
B0.endif1.1.endif: ; preds = %B0.endif1.1
tail call void @Py_DecRef(i8* %arg.b)
tail call void @Py_DecRef(i8* %arg.a)
tail call void @Py_IncRef(i8* %.38)
tail call void @Py_DecRef(i8* %.38)
store i8* %.38, i8** %retptr, align 8
ret i32 0
}
declare void @PyErr_SetString(i8*, i8*)
declare void @Py_IncRef(i8*)
declare void @Py_DecRef(i8*)
declare i8* @PyNumber_Add(i8*, i8*)
```
細心的讀者可能會注意到在生成的代碼中對`Py_IncRef`和`Py_DecRef`進行了幾次不必要的調用。目前,Numba 無法優化這些。
對象模式編譯還將嘗試識別可以為“nopython”編譯提取和靜態類型的循環。這個過程被稱為 _ 循環提升 _,并導致創建一個隱藏的 nopython 模式函數,該函數只包含從原始函數調用的循環。循環提升有助于提高需要訪問不可編譯代碼(例如 I / O 或繪圖代碼)的函數的性能,但仍包含可編譯代碼的時間密集部分。
### 7.2.4.10。階段 8:將 LLVM IR 編譯為機器代碼
在[對象模式](../glossary.html#term-object-mode)和 [nopython 模式](../glossary.html#term-nopython-mode)中,生成的 LLVM IR 由 LLVM JIT 編譯器編譯,并且機器代碼被加載到內存中。還創建了一個 Python 包裝器(在`numba.dispatcher.Dispatcher`中定義),如果生成了多個類型特化,它可以動態調度到正確版本的編譯函數(例如,對于同一版本的`float32`和`float64`版本)功能)。
通過將 [`NUMBA_DUMP_ASSEMBLY`](../reference/envvars.html#envvar-NUMBA_DUMP_ASSEMBLY) 環境變量設置為 1,可以將 LLVM 生成的機器匯編代碼轉儲到屏幕上:
```py
.globl __main__.add$1.int64.int64
.align 16, 0x90
.type __main__.add$1.int64.int64,@function
__main__.add$1.int64.int64:
addq %r8, %rcx
movq %rcx, (%rdi)
xorl %eax, %eax
retq
```
程序集輸出還將包括生成的包裝函數,該函數將 Python 參數轉換為本機數據類型。
- 1. 用戶手冊
- 1.1。 Numba 的約 5 分鐘指南
- 1.2。概述
- 1.3。安裝
- 1.4。使用@jit 編譯 Python 代碼
- 1.5。使用@generated_jit 進行靈活的專業化
- 1.6。創建 Numpy 通用函數
- 1.7。用@jitclass 編譯 python 類
- 1.8。使用@cfunc 創建 C 回調
- 1.9。提前編譯代碼
- 1.10。使用@jit 自動并行化
- 1.11。使用@stencil裝飾器
- 1.12。從 JIT 代碼 中回調到 Python 解釋器
- 1.13。性能提示
- 1.14。線程層
- 1.15。故障排除和提示
- 1.16。常見問題
- 1.17。示例
- 1.18。會談和教程
- 2. 參考手冊
- 2.1。類型和簽名
- 2.2。即時編譯
- 2.3。提前編譯
- 2.4。公用事業
- 2.5。環境變量
- 2.6。支持的 Python 功能
- 2.7。支持的 NumPy 功能
- 2.8。與 Python 語義的偏差
- 2.9。浮點陷阱
- 2.10。 Python 2.7 壽命終止計劃
- 3. 用于 CUDA GPU 的 Numba
- 3.1。概述
- 3.2。編寫 CUDA 內核
- 3.3。內存管理
- 3.4。編寫設備功能
- 3.5。 CUDA Python 中支持的 Python 功能
- 3.6。支持的原子操作
- 3.7。隨機數生成
- 3.8。設備管理
- 3.10。示例
- 3.11。使用 CUDA 模擬器 調試 CUDA Python
- 3.12。 GPU 減少
- 3.13。 CUDA Ufuncs 和廣義 Ufuncs
- 3.14。共享 CUDA 內存
- 3.15。 CUDA 陣列接口
- 3.16。 CUDA 常見問題
- 4. CUDA Python 參考
- 4.1。 CUDA 主機 API
- 4.2。 CUDA 內核 API
- 4.3。內存管理
- 5. 用于 AMD ROC GPU 的 Numba
- 5.1。概述
- 5.2。編寫 HSA 內核
- 5.3。內存管理
- 5.4。編寫設備功能
- 5.5。支持的原子操作
- 5.6。代理商
- 5.7。 ROC Ufuncs 和廣義 Ufuncs
- 5.8。示例
- 6. 擴展 Numba
- 6.1。高級擴展 API
- 6.2。低級擴展 API
- 6.3。示例:間隔類型
- 7. 開發者手冊
- 7.1。貢獻給 Numba
- 7.2。 Numba 建筑
- 7.3。多態調度
- 7.4。關于發電機的注意事項
- 7.5。關于 Numba Runtime 的注意事項
- 7.6。使用 Numba Rewrite Pass 獲得樂趣和優化
- 7.7。實時變量分析
- 7.8。上市
- 7.9。模板注釋
- 7.10。關于自定義管道的注意事項
- 7.11。環境對象
- 7.12。哈希 的注意事項
- 7.13。 Numba 項目路線圖
- 8. Numba 增強建議
- 9. 術語表