fel*_*021 74 python caching code-analysis literals python-internals
深入研究Python的源代码后,我发现它维护了一个PyInt_Objects 数组,范围从int(-5)到int(256)(@src/Objects/intobject.c)
一个小实验证明了这一点:
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
Run Code Online (Sandbox Code Playgroud)
但是如果我在py文件中一起运行这些代码(或者用分号连接它们),结果会有所不同:
>>> a = 257; b = 257; a is b
True
Run Code Online (Sandbox Code Playgroud)
我很好奇为什么它们仍然是同一个对象,所以我深入研究语法树和编译器,我想出了一个下面列出的调用层次结构:
PyRun_FileExFlags()
mod = PyParser_ASTFromFile()
node *n = PyParser_ParseFileFlagsEx() //source to cst
parsetoke()
ps = PyParser_New()
for (;;)
PyTokenizer_Get()
PyParser_AddToken(ps, ...)
mod = PyAST_FromNode(n, ...) //cst to ast
run_mod(mod, ...)
co = PyAST_Compile(mod, ...) //ast to CFG
PyFuture_FromAST()
PySymtable_Build()
co = compiler_mod()
PyEval_EvalCode(co, ...)
PyEval_EvalCodeEx()
Run Code Online (Sandbox Code Playgroud)
然后我在int(-5)之前/之后添加了一些调试代码int(256),并执行了test.py:
a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))
Run Code Online (Sandbox Code Playgroud)
输出看起来像:
DEBUG: before PyAST_FromNode
name = a
ival = 257, id = 176046536
name = b
ival = 257, id = 176046752
name = a
name = b
DEBUG: after PyAST_FromNode
run_mod
PyAST_Compile ok
id(a) = 176046536, id(b) = 176046536
Eval ok
Run Code Online (Sandbox Code Playgroud)
这意味着,在PyInt_FromLong以PyAST_FromNode变换,两个不同的csts的创建(实际上它的真实执行的ast功能),但他们后来合并.
我觉得很难理解的来源PyInt_Object和ast_for_atom(),所以我在这里寻求帮助,我会很感激,如果有人给出了一个暗示?
Bak*_*riu 90
Python在范围内缓存整数[-5, 256],因此预期该范围内的整数也是相同的.
你看到的是Python编译器在同一文本的一部分时优化相同的文字.
在Python shell中输入时,每一行都是一个完全不同的语句,在不同的时刻进行解析,因此:
>>> a = 257
>>> b = 257
>>> a is b
False
Run Code Online (Sandbox Code Playgroud)
但是如果将相同的代码放入文件中:
$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True
Run Code Online (Sandbox Code Playgroud)
只要解析器有机会分析文字的使用位置,例如在交互式解释器中定义函数时,就会发生这种情况:
>>> def test():
... a = 257
... b = 257
... print a is b
...
>>> dis.dis(test)
2 0 LOAD_CONST 1 (257)
3 STORE_FAST 0 (a)
3 6 LOAD_CONST 1 (257)
9 STORE_FAST 1 (b)
4 12 LOAD_FAST 0 (a)
15 LOAD_FAST 1 (b)
18 COMPARE_OP 8 (is)
21 PRINT_ITEM
22 PRINT_NEWLINE
23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> test()
True
>>> test.func_code.co_consts
(None, 257)
Run Code Online (Sandbox Code Playgroud)
注意编译的代码如何包含一个常量257.
总之,Python字节码编译器无法执行大规模优化(如静态类型语言),但它比您想象的要多.其中一个方面是分析文字的使用并避免重复它们.
请注意,这与缓存无关,因为它也适用于没有缓存的浮点数:
>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True
Run Code Online (Sandbox Code Playgroud)
对于更复杂的文字,如元组,它"不起作用":
>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False
Run Code Online (Sandbox Code Playgroud)
但是元组内的文字是共享的:
>>> a = (257, 258)
>>> b = (257, 258)
>>> a[0] is b[0]
False
>>> a[1] is b[1]
False
>>> a = (257, 258); b = (257, 258)
>>> a[0] is b[0]
True
>>> a[1] is b[1]
True
Run Code Online (Sandbox Code Playgroud)
关于为什么你看到这两个PyInt_Object被创建,我猜这是为了避免字面比较.例如,数字257可以用多个文字表示:
>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
Run Code Online (Sandbox Code Playgroud)
解析器有两个选择:
可能Python解析器使用第二种方法,它避免重写转换代码,并且它更容易扩展(例如它也适用于浮点数).
读取Python/ast.c文件,解析所有数字的函数是parsenumber,调用PyOS_strtoul获取整数值(对于intgers)并最终调用PyLong_FromString:
x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
if (x < 0 && errno == 0) {
return PyLong_FromString((char *)s,
(char **)0,
0);
}
Run Code Online (Sandbox Code Playgroud)
正如你可以在这里看到解析器将不会检查是否已经找到与给定值的整数,所以这就是为什么你看到两个int对象被创建的,而这也意味着,我的猜测是正确的:解析器首先创建常数并且之后才优化字节码以将相同的对象用于相等的常量.
执行此检查的代码必须位于Python/compile.c或中Python/peephole.c,因为这些是将AST转换为字节码的文件.
特别是,compiler_add_o功能似乎是这样做的.有这样的评论compiler_lambda:
/* 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;
Run Code Online (Sandbox Code Playgroud)
所以它似乎compiler_add_o用于插入函数/ lambdas等compiler_add_o的常量.函数将常量存储到一个dict对象中,从此紧接着,相等的常量将落在同一个槽中,从而在最终的字节码中产生一个常量.
| 归档时间: |
|
| 查看次数: |
6755 次 |
| 最近记录: |