Mar 7

Python int缓存的那点事[续] 不指定

felix021 @ 2013-3-7 02:57 [IT » Python] 评论(0) , 引用(0) , 阅读(12141) | Via 本站原创 | |
上一篇 说到,对于这样的一段代码:
a = 257
b = 0x101
print a is b

Python解释器会为 a 和 b 各 创建一个 PyIntObject (通过修改PyInt_FromLong打印int的id可以看出来),但是在实际的执行中,a和b却指向了同一个PyIntObject。也就是说,在执行之前,a和b已经被映射到了同一个PyIntObject。

前面说了,Python的解释执行是由以下调用链组成的:
引用

PyRun_FileExFlags()
    mod_ty *mod = PyParser_ASTFromFile() //把py源码转换成AST(Abstract Syntax Tree)
    run_mod(mod, ...) //执行AST
        co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode
            PySymtable_Build() //创建符号表
            co = compiler_mod() //编译ast为bytecode
        PyEval_EvalCode(co, ...) //执行bytecode
            PyEval_EvalCodeEx()

由于没有编译原理的基础,只能从全局上看出这些代码都做了什么,但是却很难从细节上去追查。通过修改源码我尽可能了解了 PyParser 将Python源码转换成AST的运行机制(虽然还是没有看懂tokens->cst的转换),但是run_mod的细节实在是看不懂了。于是我在StackOverflow上面提了一个问题,@Bakuriu大牛给了个hint,说是PyCompiler在处理lambda的时候,使用 compiler_add_o() 来将lambda对应的函数的__doc__设置为 PyNone:
/* Make None the first constant, so the lambda can't have a
  docstring. */
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0)
    return 0;

这里头PyNone是常量,而且又出现了 c->u->u_consts ,大有看头。

有了线索以后,突然一切都变得清晰了,简单加了些代码追,可以发现对于上面给出的代码, compiler_mod 是这样处理mod_ty *mod(也就是那棵AST)的:

引用
compiler_mod(compiler *c, mod_ty *mod)
    case Module_kind: //mod->kind = 1
        compiler_body(c, mod->v.Module.body <as> stmts)
            for (i = 0; i < asdl_seq_LEN(stmts); i++) //循环3次,因为有3个stmt
                VISIT(c, stmt, asdl_seq_GET(stmts, i)) //宏展开到compiler_visit_stmt
                    compiler_visit_stmt(c, asdl_seq_GET(stmts,i) <as> s)//访问每个stmt
                        case Assign_kind: //第一个stmt的kind = 5,表示一个赋值操作
                            //赋值操作允许 a = b = 1 所以看起来有点罗嗦,
                            //它会被解析成如下AST:
                            //Assign([AssName('a', 'OP_ASSIGN'),
                            //      AssName('b', 'OP_ASSIGN')], Const(1))
                            n = asdl_seq_LEN(s->v.Assign.targets);
                            VISIT(c, expr, s->v.Assign.value);
                            for (i = 0; i < n; i++) {
                                if (i < n - 1)
                                    ADDOP(c, DUP_TOP);
                                VISIT(c, expr, asdl_seq_GET(s->v.Assign.targets, i));
                            }

简单解释下Assign操作的代码:
    1. 获得赋值目标的数量(比如a=b=1,就是2个)
    2. "VISIT"要赋的值
    3. 挨个ADDOP(c, DUP_TOP)是告诉编译器,增加一个OPCODE=DUP_TOP

DUP_TOP 是 Duplicates the reference on top of the stack 的简写,意思是取得上次计算的值(比如对于b,就是int(1)的reference,而对于a,就是b=1的返回值,也就是b的reference)加入stack_top,这样正好把多个赋值操作串起来。

不过我关注的主要是第二条,对应的代码就是:

引用
VISIT(c, expr, s->v.Assign.value); //宏展开到compiler_visit_expr
    compiler_visit_expr(c, s->v.Assign.value <as> e)
        case Num_kind: //e->kind = 16
            ADDOP_O(c, LOAD_CONST, e->v.Num.n, consts) //宏展开到compiler_addop_o
                //这里e->v.Num.n是在CST->AST的过程中生成的PyIntObject
                //下面的c->u->u_consts是一个PyDictObject,用来保存常量对象
                compiler_addop_o(c, LOAD_CONST <as> opcode,
                                c->u->u_consts <as> dict, e->v.Num.n <as> o)
                    arg = compiler_addop_o(c, dict, o)//塞入dict
                    compiler_addop_i(c, opcode, arg) //将插入顺序作为opcode的oparg


这个compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)的作用是将一个变量o及其type组成的tuple(o, o->ob_type)塞入到dict中。但是并不是简单暴力地直接插入,它的源码大约是这样的:

static int
compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)
{
    PyObject *t, *v;
    Py_ssize_t arg;

    if (PyFloat_Check(o)) {
        //省略部分与int无关的代码
    }
    else {
        t = PyTuple_Pack(2, o, o->ob_type); //t = tuple(o, type(o))
    }

    if (t == NULL)
        return -1;

    v = PyDict_GetItem(dict, t); //看看t是否已经在dict中出现过
    if (!v) { //如果没有
        arg = PyDict_Size(dict); //获取dict的当前大小(PyIntObject)
        v = PyInt_FromLong(arg);
        if (!v) {
            Py_DECREF(t);
            return -1;
        }
        if (PyDict_SetItem(dict, t, v) < 0) { //dict[(o, type(o))] = v
            Py_DECREF(t);
            Py_DECREF(v);
            return -1;
        }
        Py_DECREF(v);
    }
    else
        arg = PyInt_AsLong(v); //如果出现过,取得之前设置的v
    Py_DECREF(t);
    return arg;
}


这个代码乍看挺诡异的,因为还与后续编译成字节码的部分有所耦合,这里大致解释一下:

(1) 对于LOAD_CONSTS, 在compiler_visit_expr里面已将dict指定为c->u->u_consts,也就是专门用来存放常量的dict
(2) 所有常量都是用 (o, type(o)) 作为 key 存进去的,并返回顺序递增的编号v,表示o是第v个存进去的常量
(3) dict是个hash表,所以往dict里面塞东西时要计算key的hash
(4) tuple的hash值是将每个元素的hash值组合起来哈希(详见tuplehash函数),类似于sdbmhash或者jshash
(5) int对象的哈希是针对int的值,int对象比较时仅比较它们的值

    尽管 a 和 b 对应的key (a, int)和(b, int)虽然不是同一个对象,但是它们的哈希值是一样的!并且!PyDict_GetItem()查找到某个slot去比较key的时候,递归地去比较key的每一个元素,而两个int的比较,“正好”是比较它们的值是否相等!

    所以,在遍历AST生成bytecode之前,两个相同的const只会在c->u->u_consts中出现1次。

    在compiler_mod函数的末尾有一个assemble(c, addNone),它是将前面生成好的opcode等数据转换成最终的bytecode,其中一些代码逻辑是这样的

        assemble(c, addNone)
            struct assembler a;
            完成一些初始化
            makecode(c, &a);
                PyObject *tmp = dict_keys_inorder(c->u->u_consts, 0); //convert to Tuple
                consts = PySequence_List(tmp); //convert to List
                ...
                PyCodeObject *co = PyCode_New(..., consts, ...);
                    co->co_consts = consts


    也就是说,这里把保存所有常量的 c->u->u_consts 按照插入元素的顺序将所有塞进来的PyObject逐个插入到 consts 里,并最后赋值给PyCodeObject的PyListObject *co_consts。而在最最后的eval环节,LOAD_CONST这个opcode会将它的oparg(就是前面 compiler_addop_i 塞进去的值,也就是compiler_addop_o返回的值)作为索引,从co_consts里取出来,PUSH到栈顶(参见Python/ceval.c +1123行),供下一个指令读取。

    于是int常量整个python的解释执行所经历的步骤都完整地串起来了。

    泪流满面,居然看懂了。




欢迎扫码关注:




转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: https://www.felix021.com/blog/feed.php
发表评论
表情
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
打开HTML
打开UBB
打开表情
隐藏
记住我
昵称   密码   *非必须
网址   电邮   [注册]