字节码处理说明

LOAD_FAST_AND_CLEAR 操作码, Expr.undef IR 节点, UndefVar 类型

Python 3.12 引入了一个新的字节码 LOAD_FAST_AND_CLEAR,它仅用于推导式(comprehensions)。常见模式是

In [1]: def foo(x):
...:      # 6 LOAD_FAST_AND_CLEAR      0 (x)  # push x and clear from scope
...:      y = [x for x in (1, 2)]             # comprehension
...:      # 30 STORE_FAST              0 (x)  # restore x
...:      return x
...:

In [2]: import dis

In [3]: dis.dis(foo)
1           0 RESUME                   0

3           2 LOAD_CONST               1 ((1, 2))
            4 GET_ITER
            6 LOAD_FAST_AND_CLEAR      0 (x)
            8 SWAP                     2
            10 BUILD_LIST               0
            12 SWAP                     2
        >>   14 FOR_ITER                 4 (to 26)
            18 STORE_FAST               0 (x)
            20 LOAD_FAST                0 (x)
            22 LIST_APPEND              2
            24 JUMP_BACKWARD            6 (to 14)
        >>   26 END_FOR
            28 STORE_FAST               1 (y)
            30 STORE_FAST               0 (x)

5          32 LOAD_FAST_CHECK          0 (x)
            34 RETURN_VALUE
        >>   36 SWAP                     2
            38 POP_TOP

3          40 SWAP                     2
            42 STORE_FAST               0 (x)
            44 RERAISE                  0
ExceptionTable:
10 to 26 -> 36 [2]

Numba 处理 LOAD_FAST_AND_CLEAR 字节码的方式与 CPython 不同,因为它依赖于静态而非动态语义。

在 Python 中,推导式可以遮蔽(shadow)来自外部函数作用域的变量。为了处理这种情况,LOAD_FAST_AND_CLEAR 会快照(snapshot)一个可能被遮蔽的变量的值,并从作用域中清除它。这给人一种推导式在新作用域中执行的错觉,尽管在 Python 3.12 中它们是完全内联的。快照的值稍后会在推导式之后使用 STORE_FAST 恢复。

由于 Numba 使用静态语义,它无法精确地模拟 LOAD_FAST_AND_CLEAR 的动态行为。相反,Numba 会检查变量是否在之前的操作码中使用,以确定它是否必须已定义。如果是,Numba 会像处理常规的 LOAD_FAST 一样处理它。否则,Numba 会发出一个 Expr.undef IR 节点,将栈值标记为未定义。类型推断会将 UndefVar 类型分配给此节点,从而允许该值被零初始化并隐式转换为其他类型。

在对象模式下,Numba 使用 _UNDEFINED 哨兵对象来指示未定义的值。

如果使用了未定义的值,Numba 不会引发 UnboundLocalError

特殊情况 1:LOAD_FAST_AND_CLEAR 可能会加载一个未定义的变量

In [1]: def foo(a, v):
...:      if a:
...:          x = v
...:      y = [x for x in (1, 2)]
...:      return x

在上面的示例中,变量 x 在列表推导式之前可能已定义,也可能未定义,这取决于 a 的真值。如果 aTrue,则 x 已定义,执行按照常见情况进行。然而,如果 aFalse,则 x 未定义。在这种情况下,Python 解释器会在 return x 行引发 UnboundLocalError。Numba 无法确定 x 是否之前已定义,因此它假定 x 已定义以避免错误。这偏离了 Python 的官方语义,因为即使 x 之前未定义,Numba 也会使用一个零初始化的 x

In [1]: from numba import njit

In [2]: def foo(a, v):
...:     if a:
...:         x = v
...:     y = [x for x in (1, 2)]
...:     return x
...:

In [3]: foo(0, 123)
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[3], line 1
----> 1 foo(0, 123)

Cell In[2], line 5, in foo(a, v)
    3     x = v
    4 y = [x for x in (1, 2)]
----> 5 return x

UnboundLocalError: cannot access local variable 'x' where it is not associated with a value

In [4]: njit(foo)(0, 123)
Out[4]: 0

如上例所示,Numba 不会引发 UnboundLocalError,并允许函数正常返回。

特殊情况 2:LOAD_FAST_AND_CLEAR 加载未定义变量

如果 Numba 能够静态确定一个变量必须是未定义的,类型系统将引发 TypingError,而不是像 Python 解释器那样引发 NameError

In [1]: def foo():
...:     y = [x for x in (1, 2)]
...:     return x
...:

In [2]: foo()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 foo()

Cell In[1], line 3, in foo()
    1 def foo():
    2     y = [x for x in (1, 2)]
----> 3     return x

NameError: name 'x' is not defined

In [3]: from numba import njit

In [4]: njit(foo)()
---------------------------------------------------------------------------
TypingError                               Traceback (most recent call last)
Cell In[4], line 1
----> 1 njit(foo)()

File /numba/numba/core/dispatcher.py:468, in _DispatcherBase._compile_for_args(self, *args, **kws)
    464         msg = (f"{str(e).rstrip()} \n\nThis error may have been caused "
    465                f"by the following argument(s):\n{args_str}\n")
    466         e.patch_message(msg)
--> 468     error_rewrite(e, 'typing')
    469 except errors.UnsupportedError as e:
    470     # Something unsupported is present in the user code, add help info
    471     error_rewrite(e, 'unsupported_error')

File /numba/numba/core/dispatcher.py:409, in _DispatcherBase._compile_for_args.<locals>.error_rewrite(e, issue_type)
    407     raise e
    408 else:
--> 409     raise e.with_traceback(None)

TypingError: Failed in nopython mode pipeline (step: nopython frontend)
NameError: name 'x' is not defined