活跃变量分析

(相关问题 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)会发出一个针对 tdel 指令。一旦我们了解了控制流图,原因就显而易见了:

         +------------------------------> 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)的情况下执行,而该变量是在循环体中定义的。