Mar
3
Python int缓存的那点事
早先翻python代码的时候是有注意到 intobject 里有缓存这档事,不过没细看。昨天有人在sf问起为什么有如下现象:
于是又翻开python的源码 python2.6.8/Objects/intobject.c ,可以看到这些代码(略做简化):
而PyFloat_Object并没有(也不适合)实现这样的缓存,所以就可以解释上面的情况了。
更进一步,可以用257来验证一下,的确是超出了缓存的范围:
然后手贱做了另一个测试,蛋疼了:
也就是说如果让解释器一次执行的话,解释器又会再优化它,让a、b引用同一个对象。//注:这里对于float和str类型的常量效果是一样的。
为了搞清楚解释器到底是怎么实现这一点的,又把代码翻出来。之前翻的时候对解释器的执行流程已经有大致的了解了。make得到的python解释器是从 Module/python.c 里的 main() 函数开始的,调用链大约是这样:
从 PyRun_FileExFlags 开始,才能看到底层代码正式登场:
注:更详细的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可以看到如下输出:
从输出可以看到,解析源码生成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缓存的那点事[续]
转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: https://www.felix021.com/blog/feed.php 。
引用
>>> a = 1.0
>>> b = 1.0
>>> a is b
False
>>> a = 1
>>> b = 1
>>> a is b
True
>>> 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;
}
#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
>>> b = 257
>>> a is b
False
然后手贱做了另一个测试,蛋疼了:
引用
>>> a = 257; b = 257; a is b
True
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()
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()
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的输出
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缓存的那点事[续]
欢迎扫码关注:
转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: https://www.felix021.com/blog/feed.php 。
xayljq
2013-3-5 21:16
似乎Python27的缓存范围又变了
felix021 回复于 2013-3-5 22:11
没有,我看了下2.7.3的源码,也还是[-5, 256]
分页: 1/1 1