字节码处理的注意事项
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
的真值。如果 a
是 True
,那么 x
被定义,执行过程如常见情况所述。然而,如果 a
是 False
,那么 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