活跃变量分析
(相关问题 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
从未在循环外部被引用。一个 del
指令会在循环的头部(BB 1)在变量定义之前为 t
发出。一旦我们了解控制流图,原因就很明显了
+------------------------------> BB4
|
|
BB 0 --> BB 1 --> BB 2 ---> BB 3
^ | |
| V V
+---------------------+
变量 t
在 BB 1 中定义。在 BB 2 中,表达式 t[i] > 1
的求值使用了 t
,如果执行走假分支并跳转到 BB 1,这是最后一次使用。在 BB 3 中,t
仅用于 res += t[i]
,如果执行走真分支,这是最后一次使用。因为 BB 3 是 BB 2 的一个传出分支,它使用了 t
,所以 t
必须在共同的前驱处被删除。最近的点是 BB 1,它没有从 BB 0 的传入边定义 t
。
或者,如果在 BB 4 处删除 t
,我们仍然需要在变量定义之前删除它,因为 BB4 可以在不执行循环体(BB 2 和 BB 3)的情况下执行,而变量正是在循环体中定义的。