活跃变量分析

(相关问题 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)的情况下执行,而变量正是在循环体中定义的。