29. 基于堆栈的执行

CPython 使用堆栈机执行大多数字节码。堆栈机使用隐式操作数堆栈,而不是在每条指令中命名显式源寄存器和目标寄存器。

在 CPython 中,该堆栈属于当前帧。它存储对 Python 对象的引用。字节码指令推送对象、弹出对象、检查对象、替换对象以及使用对象计算新结果。

一个简单的表达:python id="kz6m7x" x = a + b 在概念上执行为:text id="eurdpx" LOAD_FAST a push a LOAD_FAST b push b BINARY_OP + pop b and a, push result STORE_FAST x pop result into local x 指令流没有说:text id="ardj57" add local_a, local_b, local_x 相反,它说:```text id="jzzlmf" load a load b add top two stack values store result


## 29.1 为什么 CPython 使用堆栈机

堆栈机为字节码提供了紧凑的表示形式。许多指令不需要显式操作数位置,因为它们在堆栈顶部操作。

例如:```python id="e4954y"
return (a + b) * c
```可以表示为:```text id="nppt1h"
LOAD_FAST a
LOAD_FAST b
BINARY_OP +
LOAD_FAST c
BINARY_OP *
RETURN_VALUE
````BINARY_OP`指令不需要命名其输入寄存器。它总是从堆栈中获取操作数。

这使字节码保持简单:```text id="pr4b42"
instructions are small
operands are usually indexes or small integers
temporary values do not need names
compiler stack-depth analysis is straightforward
interpreter execution model is uniform
```寄存器机将编码更明确的操作数位置:```text id="ugbzwm"
r3 = add r1, r2
r4 = mul r3, r0
return r4
```这可以减少某些设计中的指令数量,但需要不同的字节码格式和不同的编译器策略。 CPython 历史上一直使用面向堆栈的模型。

## 29.2 帧值栈

每个执行帧都有临时值的存储空间。

从概念上讲:```text id="ia1aa5"
frame
    fast locals
    cell variables
    free variables
    value stack
```为了:```python id="39cqdr"
def f(a, b):
    return a + b
```框架可以理解为:```text id="mq1wny"
localsplus
    [0] a
    [1] b
    [2...] value stack
```执行期间:```text id="oq18jh"
LOAD_FAST a
    stack: [a]

LOAD_FAST b
    stack: [a, b]

BINARY_OP +
    stack: [a_plus_b]

RETURN_VALUE
    stack: []
    return a_plus_b
```堆栈存储`PyObject *`参考。它不存储未装箱的 C 整数、原始双精度数或表示普通字节码执行中的 Python 值的机器字。

为了`a = 2``b = 3`,堆栈保存指向Python整数对象的指针。```text id="4h720d"
stack slot 0 -> PyLongObject(2)
stack slot 1 -> PyLongObject(3)
```## 29.3 堆栈指针

解释器维护当前帧的堆栈指针。

从概念上讲:```c id="l1xymk"
PyObject **stack_pointer;
```推送将一个值写入当前堆栈槽并使指针前进。```c id="0gozoa"
*stack_pointer = value;
stack_pointer++;
```弹出将指针向后移动并读取值。```c id="xj0vaq"
stack_pointer--;
value = *stack_pointer;
```真正的 CPython 实现使用宏、专用存储、引用所有权约定和生成的指令代码。但核心操作仍然是pushpop

一个简单的堆栈跟踪:```text id="9taxcy"
initial:
    sp -> slot 0

push a:
    slot 0 = a
    sp -> slot 1

push b:
    slot 0 = a
    slot 1 = b
    sp -> slot 2

pop:
    sp -> slot 1
    value = slot 1
```堆栈指针标记下一个空闲堆栈槽。

## 29.4 堆栈效应

每条指令都有堆栈效应。

堆栈效应表示指令如何改变堆栈高度。

|说明 |流行音乐||净效应|
|---|---:|---:|---:|
|`LOAD_CONST`| 0 | 1 |`+1` |
| `LOAD_FAST`| 0 | 1 |`+1` |
| `STORE_FAST`| 1 | 0 |`-1` |
| `POP_TOP`| 1 | 0 |`-1` |
| `BINARY_OP`| 2 | 1 |`-1` |
| `RETURN_VALUE`| 1 | 0 |退出框架 |
|`CALL`|可调用和参数 |结果|变量|
|`BUILD_LIST`| | 1 |`1 - N`|

编译器使用堆栈效应来计算代码对象所需的最大堆栈大小。

该值存储在代码对象中,如下所示`co_stacksize````python id="e077y8"
def f(a, b, c):
    return (a + b) * c

print(f.__code__.co_stacksize)

co_stacksize告诉 CPython 该代码在执行期间可能需要多少临时堆栈空间。

29.5 计算二元表达式

考虑:```python id="xjqe7p" def f(a, b, c): return a + b * c


概念性的堆栈执行是:```text id="0lb9i7"
LOAD_FAST a
LOAD_FAST b
LOAD_FAST c
BINARY_OP *
BINARY_OP +
RETURN_VALUE
```一步一步

|步骤|说明 |堆栈之前 |堆栈之后 |
|---:|---|---|---|
| 1 |`LOAD_FAST a` | `[]` | `[a]`|
| 2 |`LOAD_FAST b` | `[a]` | `[a, b]`|
| 3 |`LOAD_FAST c` | `[a, b]` | `[a, b, c]`|
| 4 |`BINARY_OP *` | `[a, b, c]` | `[a, b*c]`|
| 5 |`BINARY_OP +` | `[a, b*c]` | `[a + b*c]`|
| 6 |`RETURN_VALUE` | `[result]`|返回 |

编译器选择在使用堆栈时保留 Python 表达式语义的指令顺序

## 29.6 括号和堆栈顺序

括号改变计算顺序。```python id="g8ade6"
def f(a, b, c):
    return (a + b) * c
```概念字节码:```text id="1fshyd"
LOAD_FAST a
LOAD_FAST b
BINARY_OP +
LOAD_FAST c
BINARY_OP *
RETURN_VALUE
```一步一步

|步骤|说明 |堆栈之后 |
|---:|---|---|
| 1 |`LOAD_FAST a` | `[a]`|
| 2 |`LOAD_FAST b` | `[a, b]`|
| 3 |`BINARY_OP +` | `[a+b]`|
| 4 |`LOAD_FAST c` | `[a+b, c]`|
| 5 |`BINARY_OP *` | `[(a+b)*c]`|
| 6 |`RETURN_VALUE`|返回 |

堆栈机通过计算子表达式并将其结果保留在堆栈上来自然地表示嵌套表达式

## 29.7 操作数顺序

对于二元运算操作数顺序很重要

为了:```python id="dm51wp"
left - right
```解释器必须使用正确的左操作数和右操作数调用减法

之前的堆栈`BINARY_OP -`:```text id="ghn1ei"
[left, right]
```该操作首先弹出右操作数然后弹出左操作数:```c id="8cs8ys"
right = pop();
left = pop();
result = subtract(left, right);
push(result);
```如果顺序颠倒减法除法比较和许多用户定义的运算就会出错

同样的问题出现在调用索引属性设置和解包中

## 29.8 赋值消耗堆栈值

赋值使用表达式生成的堆栈值。```python id="dk9je6"
x = a + b
```概念执行:```text id="nw82k0"
LOAD_FAST a       stack: [a]
LOAD_FAST b       stack: [a, b]
BINARY_OP +       stack: [result]
STORE_FAST x      stack: []

STORE_FAST消耗栈顶值并将其存储到本地槽中。

对于多项作业:```python id="7bq1ez" x = y = value


从概念上讲:```text id="tc6ix1"
LOAD_FAST value
COPY
STORE_FAST x
STORE_FAST y
```确切的字节码因 Python 版本而异,但堆栈规则是相同的:赋值目标消耗堆栈中的值。

## 29.9 堆栈上的函数调用

函数调用使用堆栈来排列可调用对象和参数。

为了:```python id="zluswf"
result = f(a, b)
```堆栈必须包含调用指令所需布局中的可调用对象和参数。

从概念上讲:```text id="yvk6nb"
LOAD_FAST f       stack: [f]
LOAD_FAST a       stack: [f, a]
LOAD_FAST b       stack: [f, a, b]
CALL 2            stack: [result]
STORE_FAST result stack: []
```调用指令知道位置参数的数量。它使用可调用对象和参数,调用调用机制,并推送返回值。

当函数调用包括以下内容时,函数调用会更加复杂:```text id="wi0q0p"
keyword arguments
default values
starred arguments
double-starred mappings
bound methods
callable classes
C functions
Python functions
coroutines
```但计算堆栈仍然携带立即调用操作数。

## 29.10 方法调用

方法调用经过优化,因为它们很常见。

为了:```python id="4dpcx3"
obj.method(arg)
```朴素的模型是:```text id="bxncuk"
load obj
load attribute method
create bound method object
load arg
call bound method
```为每个调用创建绑定方法对象的成本可能很高。

CPython 使用专门的方法调用字节码和调用路径来尽可能避免不必要的临时对象。

从概念上讲,优化路径试图表示:```text id="2vrzhp"
call underlying function with self and args
```而不是:```text id="a30j23"
create bound method object
then call it
```因此,方法调用的堆栈布局在实现细节上可能与普通函数调用不同。目标保持不变:安排可调用状态和参数,以便调用机制可以有效运行。

## 29.11 属性访问和堆栈值

属性访问消耗一个对象并产生一个属性值。```python id="yr0lx4"
value = obj.name
```从概念上讲:```text id="k5xpeh"
LOAD_FAST obj       stack: [obj]
LOAD_ATTR name      stack: [value]
STORE_FAST value    stack: []

LOAD_ATTR弹出或读取对象,执行属性查找,然后推送结果。

属性查找可能涉及:text id="mh8xew" type lookup descriptor protocol instance dictionary lookup slots properties custom __getattribute__ custom __getattr__ inline cache checks 从堆栈机的角度来看,操作很简单:```text id="x0kf3f" object in attribute value out


## 29.12 Subscription and Indexing

Subscription also uses the stack.```python id="rey7pr"
value = xs[i]
```概念执行:```text id="r32lp4"
LOAD_FAST xs       stack: [xs]
LOAD_FAST i        stack: [xs, i]
BINARY_SUBSCR      stack: [value]
STORE_FAST value   stack: []
```对于作业:```python id="enkjpe"
xs[i] = value
```堆栈必须保存容器、索引和分配的值。

从概念上讲:```text id="8q3nbv"
LOAD_FAST value    stack: [value]
LOAD_FAST xs       stack: [value, xs]
LOAD_FAST i        stack: [value, xs, i]
STORE_SUBSCR       stack: []
```选择确切的顺序`STORE_SUBSCR`可以一致地弹出操作数。

## 29.13 构建容器

列表、元组、集合和字典文字使用堆栈。```python id="8qazhy"
xs = [a, b, c]
```从概念上讲:```text id="q3tfjh"
LOAD_FAST a
LOAD_FAST b
LOAD_FAST c
BUILD_LIST 3
STORE_FAST xs
```堆栈演变:

|说明 |堆栈之后 |
|---|---|
|`LOAD_FAST a` | `[a]` |
| `LOAD_FAST b` | `[a, b]` |
| `LOAD_FAST c` | `[a, b, c]` |
| `BUILD_LIST 3` | `[[a, b, c]]` |
| `STORE_FAST xs` | `[]` |

`BUILD_LIST 3`消耗三个堆栈值并推送一个列表对象。

对于字典:```python id="u3ds6p"
d = {"a": x, "b": y}
```在字典构建指令使用它们之前,堆栈可能包含键和值对象。

## 29.14 拆包

拆箱与容器结构相反。```python id="ooj3e0"
a, b = pair
```从概念上讲:```text id="ro1rmt"
LOAD_FAST pair
UNPACK_SEQUENCE 2
STORE_FAST a
STORE_FAST b
```unpack 指令消耗一个可迭代对象并推送其元素。

编译器必须安排存储顺序,以便正确的变量接收正确的值。

为了:```python id="4wc70r"
a, b = [1, 2]
```解包后的堆栈在概念上可能是:```text id="ajf8h4"
[1, 2]
```然后存储消费值。

嵌套拆包:```python id="mvd1oq"
a, (b, c) = value
```需要多次解包操作和仔细的堆栈排序。

## 29.15 比较

比较消耗操作数并推送布尔结果或另一个比较结果。```python id="z499ha"
x < y
```从概念上讲:```text id="58s9qc"
LOAD_FAST x
LOAD_FAST y
COMPARE_OP <
```堆:```text id="8lxjax"
before COMPARE_OP: [x, y]
after COMPARE_OP:  [result]
```连锁比较更加微妙。```python id="97mjg8"
a < b < c
```Python 评估`b`仅一次并保留它以供第二次比较。

从概念上讲:```text id="202tno"
load a
load b
compare a < b
if false, stop
keep b
load c
compare b < c
```堆栈机必须复制或旋转值以避免计算`b`两次。

## 29.16 布尔短路

布尔表达式使用跳转和堆栈。```python id="u61h0m"
a and b
```如果`a` falsePython 返回`a`没有评估`b`

从概念上讲:```text id="j7lc2d"
LOAD_FAST a
if false, jump to end and keep a
POP_TOP
LOAD_FAST b
end:
```为了:```python id="v0ux9n"
a or b
```如果`a` truePython 返回`a`没有评估`b````text id="38razm"
LOAD_FAST a
if true, jump to end and keep a
POP_TOP
LOAD_FAST b
end:
```堆栈用于保存选定的结果。

这解释了为什么Python`and``or`返回其操作数之一,不一定`True`或者`False````python id="n4qhf6"
result = [] or [1, 2]
```返回:```text id="n0s1kg"
[1, 2]
```## 29.17 条件表达式

条件表达式:```python id="77rz4v"
x if cond else y
```使用分支。

概念字节码:```text id="3epj6l"
LOAD_FAST cond
POP_JUMP_IF_FALSE else_branch

LOAD_FAST x
JUMP end

else_branch:
LOAD_FAST y

end:
```表达式后面的堆栈只包含一个值:或者`x`或者`y`

这是一个关键的编译器不变量。无论执行哪个分支,合并点处的堆栈形状都必须兼容。

## 29.18 堆栈形状不变量

在控制流合并点,编译器必须保持一致的堆栈形状。

例子:```python id="hq40fy"
if cond:
    x = a
else:
    x = b
```在控制流重新加入之前,两个分支必须使堆栈处于兼容状态。

对于表达式级分支:```python id="25hx6g"
result = a if cond else b
```两个分支都必须在堆栈上留下一个值。```text id="6wovt1"
then branch leaves: [a]
else branch leaves: [b]
merge expects:      [one value]
```如果一个分支留下两个值,另一个分支留下一个值,则后续字节码将不知道要使用什么。

因此,堆栈正确性既是编译器的责任,也是解释器的责任。

## 29.19 循环和堆栈

循环使用跳转。堆栈必须在循环边界保持平衡。

例子:```python id="0n8atk"
while i < n:
    i += 1
```概念执行:```text id="f8q5zf"
loop_start:
    LOAD_FAST i
    LOAD_FAST n
    COMPARE_OP <
    POP_JUMP_IF_FALSE loop_end

    LOAD_FAST i
    LOAD_CONST 1
    BINARY_OP +=
    STORE_FAST i

    JUMP loop_start

loop_end:
````loop_start`,堆栈每次迭代都必须处于预期的形状。通常对于语句级循环它是空的。

如果循环体留下额外的堆栈值,则下一次迭代将开始损坏。

## 29.20 For 循环

一个`for`循环基于迭代器协议。```python id="n5tak3"
for x in xs:
    body(x)
```从概念上讲:```text id="xowkx6"
LOAD_FAST xs
GET_ITER

loop:
    FOR_ITER end
    STORE_FAST x

    LOAD_FAST body
    LOAD_FAST x
    CALL 1
    POP_TOP

    JUMP loop

end:
```迭代器在循环迭代中保留在堆栈上。

简化的堆栈视图:```text id="hiog6p"
after GET_ITER:
    [iterator]

FOR_ITER success:
    [iterator, next_value]

STORE_FAST x:
    [iterator]

loop body runs:
    [iterator]

FOR_ITER exhausted:
    []
```这是一个重要的情况,其中值有意保留在跨控制流区域的堆栈上。

## 29.21 尝试、例外和堆栈规则

异常处理还取决于堆栈状态。

为了:```python id="ehr6ve"
try:
    risky()
except ValueError:
    recover()
```口译员必须知道:```text id="tf513e"
which bytecode range is protected
where the handler starts
what stack depth to restore on exception
what exception values to make available
```当异常发生时,CPython 在进入处理程序之前会展开到已知的堆栈状态。

这是必要的,因为当堆栈包含临时值时可能会发生异常。

例子:```python id="mkevyx"
x = f(g(), h())
```如果`h()`引发,堆栈可能已经包含`f`和结果`g()`。异常处理必须正确清理这些临时变量。

## 29.22 最后块

一个`finally`块必须在返回、异常传播期间运行,`break` 或者`continue````python id="jvfmzp"
def f():
    try:
        return 1
    finally:
        cleanup()
```解释器在执行清理时必须保留挂起的返回。

从概念上讲:```text id="9zb2u4"
prepare return value
enter finally
run cleanup
if cleanup succeeds:
    resume return
if cleanup raises:
    discard pending return and propagate new exception
```堆栈和帧状态必须正确表示此待处理的控制流。`finally`不仅仅是一个分支机构。它是一个控制流拦截点。

## 29.23 With 语句

一个`with`语句编译为调用`__enter__``__exit__````python id="swotvs"
with cm() as value:
    body(value)
```概念行为:```text id="29ed2g"
manager = cm()
exit = manager.__exit__
value = manager.__enter__()
try:
    body(value)
finally:
    exit(...)
```堆栈用于保持上下文管理器退出机制可用,直到块退出。

这可以让 CPython 正确调用`__exit__`用于正常完成和异常完成。

## 29.24 堆栈值是引用

每个堆栈槽都保存对 Python 对象的引用。

这意味着堆栈操作与引用计数交互。

当指令推送对象时,它必须确保该对象保持活动状态。

当一条指令从堆栈中删除一个对象并且不再拥有它时,它必须在适当的情况下释放引用。

简化的二元运算:```c id="0lju6g"
right = pop();
left = pop();

result = PyNumber_Add(left, right);

Py_DECREF(left);
Py_DECREF(right);

if (result == NULL) {
    goto error;
}

push(result);
```确切的所有权规则因操作码和辅助函数而异。但不变量是严格的:```text id="2wi5o6"
objects on the stack must remain alive
objects removed from the stack must be released when no longer needed
```堆栈错误可能会导致内存泄漏、释放后使用、崩溃或错误结果。

## 29.25 借用和拥有的参考文献

CPython 指令通常区分借用引用和自有引用。

借用的引用意味着代码可以暂时使用该对象,但不拥有新的引用。

拥有的引用意味着代码最终必须释放它。

堆栈条目通常需要被视为拥有的或实时引用,以防止过早释放。

例如,加载一个常量`co_consts`可以从常量元组中读取借用的引用,然后在将其放入堆栈之前增加其引用计数。

从概念上讲:```c id="83om3h"
value = code->consts[index];   /* borrowed */
Py_INCREF(value);              /* now owned */
push(value);
```同样,现代 CPython 可能会使用优化的宏,但生命周期规则仍然存在。

## 29.26 堆栈和垃圾收集

值堆栈是活动根集的一部分。

如果对象位于活动帧的堆栈上,则该对象是可访问的并且不能被收集。

例子:```python id="mz56t7"
result = f(g(), h())
```在评估调用时,中间对象可能仅存在于堆栈中。它们可能尚未存储在任何局部变量或容器中。

从概念上讲:```text id="ioz8o6"
stack:
    f
    result_of_g
    result_of_h
```这些对象是活动的,因为帧堆栈引用了它们。

当帧由于返回或异常而展开时,CPython 会释放堆栈引用。

## 29.27 堆栈和回溯

如果发生异常,则必须在公开回溯之前进行堆栈清理。

例子:```python id="2054au"
def f():
    return g(h())
```如果`h()`引发,部分评估的调用`g`必须被抛弃。堆栈上的临时变量必须递减。

回溯保留帧和指令信息,但不保留任意死临时堆栈值。

这就是解释器中的异常处理代码很微妙的部分原因。

## 29.28 基于堆栈的执行和专门化

专业化不会删除堆栈机器模型。

通用操作:```text id="w24zf8"
BINARY_OP +
```可能成为整数加法、字符串连接或其他常见情况的专门操作。

但堆栈契约保持不变:```text id="5e8nnk"
input stack:  [left, right]
output stack: [result]
```这很重要。专业化可以改变内部快速路径,同时保留字节码级堆栈行为。

例如:```text id="1cdxfe"
generic BINARY_OP
    pop left and right
    call generic numeric dispatch
    push result

specialized int add
    pop left and right
    verify both are exact ints
    perform fast int path
    push result
```如果防护失败,解释器可以退回到通用路径。

## 29.29 内联缓存和堆栈效应

内联缓存将元数据存储在字节码指令附近,但它们的行为与正常堆栈值不同。

为了:```text id="6a9ugx"
LOAD_ATTR name
CACHE
CACHE
```缓存条目可能会占用字节码空间或解释器元数据,但它们不是推送到 Python 计算堆栈上的值。

的堆栈效应`LOAD_ATTR`遗迹:```text id="08gvhh"
input:  [object]
output: [attribute_value]
```缓存加速了从输入到输出的转换。

在阅读反汇编时,这种区别很重要。一些显示的条目可能代表解释器缓存机制而不是用户可见的操作。

## 29.30 堆栈深度示例

考虑:```python id="w9gktu"
def f(a, b, c, d):
    return (a + b) * (c + d)
```概念字节码:```text id="2b4bdv"
LOAD_FAST a
LOAD_FAST b
BINARY_OP +
LOAD_FAST c
LOAD_FAST d
BINARY_OP +
BINARY_OP *
RETURN_VALUE
```堆栈演变:

|步骤|说明 |堆栈|
|---:|---|---|
| 0 |开始 |`[]`|
| 1 |`LOAD_FAST a` | `[a]`|
| 2 |`LOAD_FAST b` | `[a, b]`|
| 3 |`BINARY_OP +` | `[a+b]`|
| 4 |`LOAD_FAST c` | `[a+b, c]`|
| 5 |`LOAD_FAST d` | `[a+b, c, d]`|
| 6 |`BINARY_OP +` | `[a+b, c+d]`|
| 7 |`BINARY_OP *` | `[(a+b)*(c+d)]`|
| 8 |`RETURN_VALUE`|返回 |

在此概念序列中,最大堆栈深度为 3

编译器静态地计算它。

## 29.31 评估顺序

Python 定义了求值顺序。 CPython 字节码必须保留它。

对于函数调用参数:```python id="mm75rh"
f(a(), b(), c())
```调用从左到右发生。

从概念上讲:```text id="1fhff2"
load f
call a()
push result of a
call b()
push result of b
call c()
push result of c
call f with three arguments
```如果`b()`加薪,`c()`一定不能跑。

堆栈机必须保留此顺序,同时还清除错误时的临时值。

## 29.32 副作用

由于 Python 表达式可能有副作用,因此堆栈执行不能任意重新排序操作。

例子:```python id="1d9mjp"
items[i()] = value()
```致电`i()``value()`必须按照 Python 指定的赋值评估顺序发生。

另一个例子:```python id="hz0uwv"
f(x, x := 10)
```赋值表达式、函数调用、属性访问和索引都会影响程序状态。

编译器必须生成尊重这些副作用的字节码。堆栈只是一种执行机制,而不是重新排列语义的许可证。

## 29.33 堆栈机与 Python 调用堆栈

计算堆栈和Python 调用堆栈是不同的。

评估堆栈位于一帧内:```text id="xm2eri"
frame f
    value stack: temporary operands
```Python 调用堆栈是一个帧链:```text id="6gz9pq"
frame a
    calls frame b
        calls frame c
```函数调用创建一个新框架。该新框架有自己的评估堆栈。

例子:```python id="hh6rmw"
def a():
    return b(1 + 2)

def b(x):
    return x * 10
```期间`a`,表达式`1 + 2`用途`a`的值栈。什么时候`b`被称为,`b`获得自己的框架和自己的值堆栈。

## 29.34 堆栈机与 C 堆栈

CPython 值堆栈也不同于本机 C 堆栈。

|堆栈|意义|
|---|---|
| Python 值栈 |一帧内的临时 Python 对象操作数 |
| Python 调用堆栈 | Python 框架链 |
| C 堆栈 |解释器和 C 函数使用的本机调用框架 |

回溯显示 Python 调用堆栈,而不是完整的 C 堆栈。

值堆栈通常在回溯中不可见。它是解释器内部的执行结构。

## 29.35 堆栈损坏

解释器依赖于精确的堆栈规则。

如果操作码弹出太多值、未能弹出足够的值、推送错误数量的值或错误处理错误路径,则帧将变为无效。

可能的后果:```text id="lbfom7"
wrong Python result
crash
memory leak
use-after-free
invalid traceback
corrupted exception state
security-sensitive memory bug
```这就是为什么 CPython 字节码指令必须具有明确定义的堆栈效果,以及编译器输出必须一致的原因。

对于核心开发来说,检查堆栈效果并不是表面的。这是一个正确性条件。

## 29.36 反汇编堆栈行为

使用`dis`研究堆栈执行。```python id="ufy4lp"
import dis

def f(a, b, c):
    return (a + b) * c

dis.dis(f)
```然后手动跟踪堆栈:```text id="e45wnw"
start: []
LOAD_FAST a       [a]
LOAD_FAST b       [a, b]
BINARY_OP +       [a+b]
LOAD_FAST c       [a+b, c]
BINARY_OP *       [(a+b)*c]
RETURN_VALUE      return
```这个练习很有用,因为它连接了源代码、字节码和帧执行。

## 29.37 一个小型堆栈解释器

用于算术的最小堆栈解释器可以显示模型。```python id="e126pg"
LOAD_CONST = "LOAD_CONST"
ADD = "ADD"
MUL = "MUL"
RETURN = "RETURN"

def run(code, consts):
    stack = []

    for op, arg in code:
        if op == LOAD_CONST:
            stack.append(consts[arg])

        elif op == ADD:
            right = stack.pop()
            left = stack.pop()
            stack.append(left + right)

        elif op == MUL:
            right = stack.pop()
            left = stack.pop()
            stack.append(left * right)

        elif op == RETURN:
            return stack.pop()

    raise RuntimeError("missing RETURN")
```跑步:```python id="57jzcg"
code = [
    (LOAD_CONST, 0),
    (LOAD_CONST, 1),
    (ADD, None),
    (LOAD_CONST, 2),
    (MUL, None),
    (RETURN, None),
]

consts = [2, 3, 4]

print(run(code, consts))
```这计算:```text id="jwwxxd"
(2 + 3) * 4
```该模型比 CPython 小得多,但操作数堆栈的思想是相同的。

## 29.38 将本地变量添加到玩具解释器中

稍微丰富一点的口译员可以为当地人提供支持。```python id="w8sn78"
LOAD_FAST = "LOAD_FAST"
STORE_FAST = "STORE_FAST"
LOAD_CONST = "LOAD_CONST"
ADD = "ADD"
RETURN = "RETURN"

def run(code, consts, locals_):
    stack = []

    for op, arg in code:
        if op == LOAD_FAST:
            stack.append(locals_[arg])

        elif op == STORE_FAST:
            locals_[arg] = stack.pop()

        elif op == LOAD_CONST:
            stack.append(consts[arg])

        elif op == ADD:
            right = stack.pop()
            left = stack.pop()
            stack.append(left + right)

        elif op == RETURN:
            return stack.pop()

    raise RuntimeError("missing RETURN")
```为了:```python id="7k6vgh"
x = a + 1
return x
```字节码可以是:```python id="iq1afx"
code = [
    (LOAD_FAST, "a"),
    (LOAD_CONST, 0),
    (ADD, None),
    (STORE_FAST, "x"),
    (LOAD_FAST, "x"),
    (RETURN, None),
]

print(run(code, [1], {"a": 41}))
```尽管 CPython 在热路径中使用索引和对象指针而不是 Python 字典,但这演示了与 CPython 的快速局部变量加值堆栈相同的模式。

## 29.39 堆栈模型解释什么

堆栈模型解释了许多 CPython 行为:```text id="to5h59"
why bytecode instructions are small
why expression evaluation has a natural order
why co_stacksize exists
why local variables can be slot-indexed
why disassembly can be manually simulated
why exception paths must clean temporary values
why function calls have careful stack layouts
why specialization must preserve stack effects
why compiler correctness depends on stack balance
```一旦您可以跟踪函数的堆栈,CPython 字节码就会变得更容易阅读。

## 29.40 常见误解

|误会 |正确型号 |
|---|---|
|堆栈存储原始机器整数 |它存储对Python对象的引用|
|值栈与调用栈相同 |每个帧都有自己的值栈|
|字节码指令命名所有操作数 |许多操作数在堆栈上是隐式的 |
|堆栈顺序并不重要 |操作数顺序至关重要|
|分支可以留下任意堆叠形状 |合并点需要兼容的堆栈形状 |
|专业化移除堆栈模型 |它保持相同的堆栈契约|
|`co_stacksize`是动态的|它是根据字节码堆栈效果计算的 |
|临时变量总是存在于局部变量中 |许多人只靠价值堆栈生存|

## 29.41 阅读策略

要研究基于堆栈的执行,请使用小示例。

从以下开始:```python id="5xvmyd"
def f(a, b):
    return a + b
```然后:```python id="l91624"
def g(a, b, c):
    return (a + b) * c
```然后:```python id="0pq8z6"
def h(xs):
    total = 0
    for x in xs:
        total += x
    return total
```对于每个函数:```python id="fl0izq"
import dis
dis.dis(function_name)
```追踪:```text id="lfc0ha"
which instructions push
which instructions pop
where the stack is empty
where values remain across jumps
where calls consume arguments
where returns consume values
```这个过程建立了口译员的具体心理模型。

## 29.42 章节总结

CPythons bytecode execution is stack-based.每个帧都有一个用于临时 Python 对象引用的值堆栈。字节码指令将值压入堆栈,消耗堆栈中的值,并将结果留给后续指令。

The essential pattern is:```text id="ebvtmi"
load operands
operate on top of stack
push result
store, return, call, branch, or continue
```该模型解释了表达式求值、局部变量访问、调用、循环、异常清理、容器构造、解包、专门化和编译器堆栈分析。

堆栈机的形状很简单,但它是 CPython 执行的核心。理解它可以使字节码可读并使评估循环具体化。