Skip to content

通过这篇文章我们初步了解了一下 Python 语法树的优化,《Python 内核源码解析:初步了解语法树 AST 优化的过程》。接下来我们将继续深入研究 Python 语法树的优化过程。

除了常量折叠优化外,Python 还有其它的语法树优化手段。

fold_binop

如果表达式的左值和右值都是常量,那么可以对表达式进行折叠优化。如 1 + 2,可以直接折叠成 3。这个操作是通过调用 PyNumber_Add 函数来实现数值的相加,那么最终的结果就是一个常量。除了加法外,还有减法、乘法、除法、位运算等操作。由于上一篇文章已经介绍了这种折叠优化,这里就不再赘述。

make_const

新的 val 是被计算出来的一个常量,如果 val 为 NULL,这跳过。如果 val 不为空,则需要给 val 分配新的内存,将 val 赋值给 node 的 Constant.value,并将 node 的 kind(类型)设置为 Constant_kind。

make_const 在很多地方都会被调用,如 fold_binop、fold_unaryop、fold_iter、fold_compare、fold_subscr、fold_tuple 等函数中。

c
static int
make_const(expr_ty node, PyObject *val, PyArena *arena)
{
    // Even if no new value was calculated, make_const may still
    // need to clear an error (e.g. for division by zero)
    if (val == NULL) {
        if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) {
            return 0;
        }
        PyErr_Clear();
        return 1;
    }
    if (_PyArena_AddPyObject(arena, val) < 0) {
        Py_DECREF(val);
        return 0;
    }
    node->kind = Constant_kind;
    node->v.Constant.kind = NULL;
    node->v.Constant.value = val;
    return 1;
}

fold_unaryop

一元运算操作符的折叠优化,如 not、-、+、~ 等操作符。如果操作数不是常量,那么直接返回。否则,根据操作符的类型,调用对应的函数进行计算。如 not 操作符,调用 unary_not 函数,- 操作符,调用 PyNumber_Negative 函数,+ 操作符,调用 PyNumber_Positive 函数,~ 操作符,调用 PyNumber_Invert 函数。

c
static int
fold_unaryop(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    expr_ty arg = node->v.UnaryOp.operand;

    if (arg->kind != Constant_kind) {
        /* Fold not into comparison */
        if (node->v.UnaryOp.op == Not && arg->kind == Compare_kind &&
                asdl_seq_LEN(arg->v.Compare.ops) == 1) {
            /* Eq and NotEq are often implemented in terms of one another, so
               folding not (self == other) into self != other breaks implementation
               of !=. Detecting such cases doesn't seem worthwhile.
               Python uses </> for 'is subset'/'is superset' operations on sets.
               They don't satisfy not folding laws. */
            cmpop_ty op = asdl_seq_GET(arg->v.Compare.ops, 0);
            switch (op) {
            case Is:
                op = IsNot;
                break;
            case IsNot:
                op = Is;
                break;
            case In:
                op = NotIn;
                break;
            case NotIn:
                op = In;
                break;
            // The remaining comparison operators can't be safely inverted
            case Eq:
            case NotEq:
            case Lt:
            case LtE:
            case Gt:
            case GtE:
                op = 0; // The AST enums leave "0" free as an "unused" marker
                break;
            // No default case, so the compiler will emit a warning if new
            // comparison operators are added without being handled here
            }
            if (op) {
                asdl_seq_SET(arg->v.Compare.ops, 0, op);
                COPY_NODE(node, arg);
                return 1;
            }
        }
        return 1;
    }

    typedef PyObject *(*unary_op)(PyObject*);
    static const unary_op ops[] = {
        [Invert] = PyNumber_Invert,     // ~
        [Not] = unary_not,              // not
        [UAdd] = PyNumber_Positive,     // +
        [USub] = PyNumber_Negative,     // -
    };
    PyObject *newval = ops[node->v.UnaryOp.op](arg->v.Constant.value);
    return make_const(node, newval, arena);
}

Python 优化示例,按语法来说,not 1、~ 1、- 1、+ 1 都是由两个部分组成的,一个是操作符,一个是操作数。在 Python 语法树中,这些操作符都是一元操作符,操作数都是常量。在 Python 语法树优化过程中,如果操作数是常量,则可以直接计算出结果。如下所示,not 1、~ 1、- 1、+ 1 都被优化成了常量。

py
import dis
code = """
a = not 1
b = ~ 1
c = - 1
d = + 1
"""
dis.dis(code)

优化后的字节码

text
  2           0 LOAD_CONST               0 (False)
              2 STORE_NAME               0 (a)

  3           4 LOAD_CONST               1 (-2)
              6 STORE_NAME               1 (b)

  4           8 LOAD_CONST               2 (-1)
             10 STORE_NAME               2 (c)

  5          12 LOAD_CONST               3 (1)
             14 STORE_NAME               3 (d)
             16 LOAD_CONST               4 (None)
             18 RETURN_VALUE

fold_iter

对于列表和集合,如果列表或集合中包含星号表达式,则直接返回。否则,将列表或集合转换成元组或冻结集合。如果转换成功,则将新的值赋值给 node 的 Constant.value,并将 node 的类型设置为 Constant_kind。

c
static int
fold_iter(expr_ty arg, PyArena *arena, _PyASTOptimizeState *state)
{
    PyObject *newval;
    if (arg->kind == List_kind) {
        /* First change a list into tuple. */
        asdl_expr_seq *elts = arg->v.List.elts;
        Py_ssize_t n = asdl_seq_LEN(elts);
        for (Py_ssize_t i = 0; i < n; i++) {
            expr_ty e = (expr_ty)asdl_seq_GET(elts, i);
            if (e->kind == Starred_kind) {
                return 1;
            }
        }
        expr_context_ty ctx = arg->v.List.ctx;
        arg->kind = Tuple_kind;
        arg->v.Tuple.elts = elts;
        arg->v.Tuple.ctx = ctx;
        /* Try to create a constant tuple. */
        newval = make_const_tuple(elts);
    }
    else if (arg->kind == Set_kind) {
        newval = make_const_tuple(arg->v.Set.elts);
        if (newval) {
            Py_SETREF(newval, PyFrozenSet_New(newval));
        }
    }
    else {
        return 1;
    }
    return make_const(arg, newval, arena);
}

fold_iter 需要一定的条件才会被触发优化。

fold_compare

对于比较操作符,如果操作符是 in 或 not in,且比较的对象是列表或集合,则将列表或集合转换成元组或冻结集合。如果转换成功,则将新的值赋值给 node 的 Constant.value,并将 node 的类型设置为 Constant_kind。

c
static int
fold_compare(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    asdl_int_seq *ops;
    asdl_expr_seq *args;
    Py_ssize_t i;

    ops = node->v.Compare.ops;
    args = node->v.Compare.comparators;
    /* TODO: optimize cases with literal arguments. */
    /* Change literal list or set in 'in' or 'not in' into
       tuple or frozenset respectively. */
    i = asdl_seq_LEN(ops) - 1;
    int op = asdl_seq_GET(ops, i);
    if (op == In || op == NotIn) {
        if (!fold_iter((expr_ty)asdl_seq_GET(args, i), arena, state)) {
            return 0;
        }
    }
    return 1;
}

Python 示例

py
import dis
code = """
1 in [1, 2, 3]
"""
dis.dis(code)

优化后的字节码,可以看到列表被优化成了元组,元组的值是常量。

text
  2           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 ((1, 2, 3))
              4 CONTAINS_OP              0
              6 RETURN_VALUE

fold_subscr

对于下标操作符,如果下标操作符的上下文不是 Load,或者下标操作符的值和下标不是常量,则直接返回。否则,调用 PyObject_GetItem 函数获取新的值。如果获取成功,则将新的值赋值给 node 的 Constant.value,并将 node 的类型设置为 Constant_kind。

c
static int
fold_subscr(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    PyObject *newval;
    expr_ty arg, idx;

    arg = node->v.Subscript.value;
    idx = node->v.Subscript.slice;
    if (node->v.Subscript.ctx != Load ||
            arg->kind != Constant_kind ||
            idx->kind != Constant_kind)
    {
        return 1;
    }

    newval = PyObject_GetItem(arg->v.Constant.value, idx->v.Constant.value);
    return make_const(node, newval, arena);
}
py
import ast
import dis

code = """\
1 in [1, 2, 3]
4 not in [2]
for i in [3]:
    pass
"""
print(ast.dump(ast.parse(code, mode='exec'), indent=4))
# Module(
#     body=[
#         Expr(
#             value=Compare(
#                 left=Constant(value=1),
#                 ops=[
#                     In()],
#                 comparators=[
#                     List(
#                         elts=[
#                             Constant(value=1),
#                             Constant(value=2),
#                             Constant(value=3)],
#                         ctx=Load())])),
#         Expr(
#             value=Compare(
#                 left=Constant(value=4),
#                 ops=[
#                     NotIn()],
#                 comparators=[
#                     List(
#                         elts=[
#                             Constant(value=2)],
#                         ctx=Load())])),
#         For(
#             target=Name(id='i', ctx=Store()),
#             iter=List(
#                 elts=[
#                     Constant(value=3)],
#                 ctx=Load()),
#             body=[
#                 Pass()],
#             orelse=[])],
#     type_ignores=[])
dis.dis(code)
#   1           0 LOAD_CONST               0 (1)
#               2 LOAD_CONST               1 ((1, 2, 3))
#               4 CONTAINS_OP              0
#               6 POP_TOP

#   2           8 LOAD_CONST               2 (4)
#              10 LOAD_CONST               3 ((2,))
#              12 CONTAINS_OP              1
#              14 POP_TOP

#   3          16 LOAD_CONST               4 ((3,))
#              18 GET_ITER
#         >>   20 FOR_ITER                 2 (to 26)
#              22 STORE_NAME               0 (i)

#   4          24 JUMP_ABSOLUTE           10 (to 20)

#   3     >>   26 LOAD_CONST               5 (None)
#              28 RETURN_VALUE

下面这个例子可以看到下标操作符的折叠优化,字符串的下标直接返回对应的字符。

打印出来的抽象语法树可以看到,语法树结构不满足退出的判断条件,通过执行 PyObject_GetItem 获取到下标对应的字符。

py
import ast
import dis

code = """\
"1223"[1]
"""
print(ast.dump(ast.parse(code, mode='eval'), indent=4))
# Expression(
#     body=Subscript(
#         value=Constant(value='1223'),
#         slice=Constant(value=1),
#         ctx=Load()))
dis.dis(code)
#   1           0 LOAD_CONST               0 ('2')
#               2 RETURN_VALUE

fold_tuple

对于元组,如果元组的上下文不是 Load,则直接返回。否则,调用 make_const_tuple 函数将元组转换成常量元组。如果转换成功,则将新的值赋值给 node 的 Constant.value,并将 node 的类型设置为 Constant_kind。

c
static int
fold_tuple(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    PyObject *newval;

    if (node->v.Tuple.ctx != Load)
        return 1;

    newval = make_const_tuple(node->v.Tuple.elts);
    return make_const(node, newval, arena);
}

如下示例可以看到元组的折叠优化,直接返回对应的常量值。

py
import ast
import dis

code = """\
(1, 2, 3)
"""
print(ast.dump(ast.parse(code, mode='exec'), indent=4))
# Module(
#     body=[
#         Expr(
#             value=Tuple(
#                 elts=[
#                     Constant(value=1),
#                     Constant(value=2),
#                     Constant(value=3)],
#                 ctx=Load()))],
#     type_ignores=[])
dis.dis(code)
#   1           0 LOAD_CONST               0 ((1, 2, 3))
#               2 RETURN_VALUE

思考

通过这篇文章,我们了解了 Python 语法树的优化过程。在 Python 语法树的优化过程中,有很多种优化手段,如常量折叠优化、一元运算操作符的折叠优化、列表和集合的折叠优化、比较操作符的折叠优化、下标操作符的折叠优化、元组的折叠优化等。这些优化手段可以有效地提高 Python 程序的执行效率。

实际上语法树还有很多的优化技术,如常量折叠、循环展开、循环不变量外提、循环合并、循环分裂、循环消除、循环划分、循环交换、循环展开、循环融合、循环滚动、循环剥离、循环分块、循环分解、循环分块、循环分解等等

如下简单的了解一下 Python 没有做到的优化。循环消除,循环 0 次毫无意义,但是 Python 却生成了很多的字节码。

py
for i in range(0):
   print(1)

输出的字节码如下

text
  2           0 LOAD_NAME                0 (range)
              2 LOAD_CONST               0 (0)
              4 CALL_FUNCTION            1
              6 GET_ITER
        >>    8 FOR_ITER                 6 (to 22)
             10 STORE_NAME               1 (i)

  3          12 LOAD_NAME                2 (print)
             14 LOAD_CONST               1 (1)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_ABSOLUTE            4 (to 8)

  2     >>   22 LOAD_CONST               2 (None)
             24 RETURN_VALUE