标题:Python int缓存的那点事 出处:Felix021 时间:Sun, 03 Mar 2013 02:42:04 +0000 作者:felix021 地址:https://www.felix021.com/blog/read.php?2105 内容: 早先翻python代码的时候是有注意到 intobject 里有缓存这档事,不过没细看。昨天有人在sf问起为什么有如下现象:引用 >>> a = 1.0 >>> b = 1.0 >>> a is b False >>> a = 1 >>> b = 1 >>> a is b True 于是又翻开python的源码 python2.6.8/Objects/intobject.c ,可以看到这些代码(略做简化):#define NSMALLPOSINTS 257 #define NSMALLNEGINTS 5 /* References to small integers are saved in this array so that they can be shared. The integers that are saved are those in the range -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive). */ static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS]; PyObject * PyInt_FromLong(long ival) { register PyIntObject *v; //如果 -5 <= ival && ival < 257, 命中缓存~ if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { v = small_ints[ival + NSMALLNEGINTS]; Py_INCREF(v); return (PyObject *) v; } if (free_list == NULL) { //这个是另一个优化,相当于内存池,用链表实现 if ((free_list = fill_free_list()) == NULL) return NULL; } /* Inline PyObject_New */ v = free_list; free_list = (PyIntObject *)Py_TYPE(v); PyObject_INIT(v, &PyInt_Type); v->ob_ival = ival; return (PyObject *) v; } 而PyFloat_Object并没有(也不适合)实现这样的缓存,所以就可以解释上面的情况了。 更进一步,可以用257来验证一下,的确是超出了缓存的范围:引用 >>> a = 257 >>> b = 257 >>> a is b False 然后手贱做了另一个测试,蛋疼了:引用 >>> a = 257; b = 257; a is b True 也就是说如果让解释器一次执行的话,解释器又会再优化它,让a、b引用同一个对象。//注:这里对于float和str类型的常量效果是一样的。 为了搞清楚解释器到底是怎么实现这一点的,又把代码翻出来。之前翻的时候对解释器的执行流程已经有大致的了解了。make得到的python解释器是从 Module/python.c 里的 main() 函数开始的,调用链大约是这样: 引用 main() @Modules/python.c Py_Main() @Modules/main.c PyRun_AnyFileExFlags() @Python/pythonrun.c PyRun_SimpleFileExFlags PyRun_FileExFlags() 从 PyRun_FileExFlags 开始,才能看到底层代码正式登场: 引用 PyRun_FileExFlags() mod_ty *mod = PyParser_ASTFromFile() //把文件转换成AST(Abstract Syntax Tree) node *n = PyParser_ParseFileFlagsEx() //生成CST(Concrete Syntax Tree) parsetoke() //逐个解析token ps = PyParser_New() for (;;) PyTokenizer_Get() //获取下一个token PyParser_AddToken(ps, ...) //将token加入到CST中 mod = PyAST_FromNode(n, ...) //将CST转换成AST 递归调用 ast_for_xxx 生成AST,同时过滤CST中的冗余信息 其中ast_for_atom中调用了parsenumber, 它调用PyInt_FromLong() run_mod(mod, ...) //执行AST co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode PyFuture_FromAST() PySymtable_Build() //创建符号表 co = compiler_mod() //编译ast为bytecode PyEval_EvalCode(co, ...) //执行bytecode PyEval_EvalCodeEx() 注:更详细的Python编译解释流程可参见这一系列: http://blog.csdn.net/atfield/article/category/256448 通过加入一些调试代码,可以窥探到内部的执行流。例如,在PyParser_AddToken中输出token的名称和类型编码;在PyParser_ParseFileFlagsEx()之后调用PyNode_ListTree(),可以看到生成的CST树(修改list1node()可以让它打印出更容易阅读的版本);修改PyInt_FromLong(),让它在ival=257的时候输出创建的object的id(CPython实现中 id 其实就是指针的值)。加上一些代码以后,编译python,执行test.py可以看到如下输出: 引用 felix021@ubuntu-server:~/src/python2.7-2.7.3$ cat test.py a = 257 b = 0x101 print a is b felix021@ubuntu-server:~/src/python2.7-2.7.3$ ./python -d test.py PyParser_ParseFileFlagsEx type = 1, token: [a] type = 22, token: [=] type = 2, token: [257] type = 4, token: [] type = 1, token: [b] type = 22, token: [=] type = 2, token: [0x101] type = 4, token: [] type = 1, token: [print] type = 1, token: [a] type = 1, token: [is] type = 1, token: [b] type = 4, token: [] type = 4, token: [] type = 0, token: [] PyNode_ListTree: <1>a //type=1表示是NAME <22>= <2>257 //type=2表示是NUMBER <4> //这是NEWLINE <1>b <22>= <2>0x101 <4> <1>print <1>a <1>is <1>b <4> <4> <0> //这是ENDMARKER Before PyAST_FromNode name = a ival = 257, id = 22699048 //注意这个id和下一个id不一样 name = b ival = 257, id = 22698784 name = b name = a After PyAST_FromNode True #这一行是print a is b的输出 从输出可以看到,解析源码生成CST的时候(输出的CST已经滤掉了非TERMINAL的node),每个token还保留着原始的字符串(例如0x101和257),而在CST到AST的转换过程中(PyAST_FromNode),解释器为每一个NUMBER都创建了一个PyIntObject。然而在程序运行的最终结果里可以看到,a is b的结果是True,也就是说,从AST转换到CFG并执行(run_mod)的过程中,解释器做了适量的优化,将 a 和 b 都指向了同一个 int 对象。 由于对CFG不熟,相应的代码还不太看得懂,所以暂时只能烂尾了,如果以后看懂了,再来补充。 续集:Python int缓存的那点事[续] Generated by Bo-blog 2.1.0