活跃变量分析
(相关问题 https://github.com/numba/numba/pull/1611)
Numba 使用引用计数进行垃圾回收,这是一种需要编译器协作的技术。Numba IR 编码了必须插入 decref 的位置。这些位置由活跃变量分析确定。对应的源代码是 https://github.com/numba/numba/blob/main/numba/interpreter.py 中的 _insert_var_dels()
方法。
在Python语义中,一旦在函数内部定义了一个变量,它就会一直存在,直到该变量被显式删除或函数作用域结束。然而,Numba在编译过程中通过分析代码来确定每个变量的生命周期的最小界限,这取决于其定义和使用情况。一旦一个变量变得不可访问,就会在最接近的基本块(无论是下一个块的开始还是当前块的结束)插入一个 del
指令。这意味着变量可以比常规Python代码更早地被释放。
实时变量分析的行为会影响编译代码的内存使用。在内部,Numba 不区分临时变量和用户变量。由于每个操作至少生成一个临时变量,如果这些变量不能尽快释放,函数可能会积累大量临时变量。我们的生成器实现可以从早期释放变量中受益,这减少了每个yield点挂起状态的大小。
关于实时变量分析行为的注释
变量在定义前被删除
(相关问题: https://github.com/numba/numba/pull/1738)
当变量的生命周期被限制在循环体内(其定义和使用不超出循环体),例如:
def f(arr):
# BB 0
res = 0
# BB 1
for i in (0, 1):
# BB 2
t = arr[i]
if t[i] > 1:
# BB 3
res += t[i]
# BB 4
return res
变量 t
在循环外部从未被引用。在变量定义之前,循环的头部(BB 1)会发出一个针对 t
的 del
指令。一旦我们了解了控制流图,原因就显而易见了:
+------------------------------> BB4
|
|
BB 0 --> BB 1 --> BB 2 ---> BB 3
^ | |
| V V
+---------------------+
变量 t
在 BB 1 中定义。在 BB 2 中,t[i] > 1
的评估使用 t
,如果执行走的是假分支并跳转到 BB 1,这是 t
的最后一次使用。在 BB 3 中,t
仅在 res += t[i]
中使用,如果执行走的是真分支,这是 t
的最后一次使用。因为 BB 3,即 BB 2 的一个外向分支使用了 t
,所以 t
必须在公共前驱处删除。最近的点是 BB 1,它没有从 BB 0 的入边定义 t
。
另外,如果在 BB 4 删除了 t
,我们仍然需要在定义之前删除该变量,因为 BB4 可以在不执行循环体(BB 2 和 BB 3)的情况下执行,而该变量是在循环体中定义的。