字节码处理的注意事项

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

Python 3.12 引入了一个新的字节码 LOAD_FAST_AND_CLEAR,它仅在推导式中使用。常见的模式是:

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中,推导式可以遮蔽封闭函数作用域中的变量。为了处理这个问题,LOAD_FAST_AND_CLEAR 会快照一个可能被遮蔽的变量的值,并将其从作用域中清除。这给人一种推导式在新作用域中执行的错觉,尽管它们在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