Mar
7
Python int缓存的那点事[续]
上一篇 说到,对于这样的一段代码:
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:
这里头PyNone是常量,而且又出现了 c->u->u_consts ,大有看头。
有了线索以后,突然一切都变得清晰了,简单加了些代码追,可以发现对于上面给出的代码, compiler_mod 是这样处理mod_ty *mod(也就是那棵AST)的:
简单解释下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,这样正好把多个赋值操作串起来。
不过我关注的主要是第二条,对应的代码就是:
这个compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)的作用是将一个变量o及其type组成的tuple(o, o->ob_type)塞入到dict中。但是并不是简单暴力地直接插入,它的源码大约是这样的:
这个代码乍看挺诡异的,因为还与后续编译成字节码的部分有所耦合,这里大致解释一下:
(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,其中一些代码逻辑是这样的
也就是说,这里把保存所有常量的 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 。
a = 257
b = 0x101
print a is b
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;
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));
}
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_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;
}
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
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 。