python - 解释器维护的整数缓存有什么用?

标签 python caching code-analysis literals python-internals

深入研究 Python 的源代码后,我发现它维护了一个 PyInt_Object 数组,范围从 int(-5)int(256) (@src/Objects/intobject.c)

一个小实验证明了这一点:

>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False

但是如果我在一个 py 文件中一起运行这些代码(或者用分号连接它们),结果是不同的:

>>> a = 257; b = 257; a is b
True

我很好奇为什么它们仍然是同一个对象,所以我深入研究了语法树和编译器,我想出了下面列出的调用层次结构:

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()

然后我在 PyInt_FromLongPyAST_FromNode 之前/之后添加了一些调试代码,并执行了一个 test.py:

a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))

输出如下:

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

这意味着在 cstast 的转换过程中,创建了两个不同的 PyInt_Object(实际上它是在 ast_for_atom 中执行的) () 函数),但它们后来被合并。

我发现很难理解 PyAST_CompilePyEval_EvalCode 中的源代码,所以我在这里寻求帮助,如果有人给出一个我将不胜感激提示?

最佳答案

Python 缓存 [-5, 256] 范围内的整数, 所以该范围内的整数通常是 but not always相同。

您看到的 257 是 Python 编译器在同一代码对象中编译时优化相同的文字。

在 Python shell 中键入时,每一行都是完全不同的语句,分别进行解析和编译,因此:

>>> a = 257
>>> b = 257
>>> a is b
False

但是如果你把相同的代码放到一个文件中:

$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True

只要编译器有机会一起分析文字,就会发生这种情况,例如在交互式解释器中定义函数时:

>>> 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)

请注意编译后的代码如何包含 257 的单个常量。

总而言之,Python 字节码编译器无法执行大量优化(如静态类型语言),但它所做的比您想象的要多。其中之一是分析文字的用法并避免重复它们。

请注意,这与缓存无关,因为它也适用于没有缓存的 float :

>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True

对于更复杂的文字,比如元组,它“不起作用”:

>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False

但是元组中的文字是共享的:

>>> 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

(请注意,常量折叠和窥孔优化器甚至可以在错误修复版本之间改变行为,因此哪些示例返回 TrueFalse 基本上是任意的,并且将来会改变).


关于您看到创建了两个 PyInt_Object 的原因,我猜测这样做是为了避免文字比较。例如,数字 257 可以用多个文字表示:

>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257

解析器有两种选择:

  • 在创建整数之前将文字转换为一些公共(public)基数,并查看文字是否等价。然后创建一个整数对象。
  • 创建整数对象并查看它们是否相等。如果是,则只保留一个值并将其分配给所有文字,否则,您已经有要分配的整数。

可能 Python 解析器使用第二种方法,它避免重写转换代码并且更容易扩展(例如它也适用于 float )。


读取Python/ast.c文件,解析所有数字的函数是parsenumber,调用PyOS_strtoul获取整数值(对于整数)并最终调用 PyLong_FromString:

    x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
    if (x < 0 && errno == 0) {
        return PyLong_FromString((char *)s,
                                 (char **)0,
                                 0);
    }

正如您在这里看到的,解析器检查它是否已经找到具有给定值的整数,因此这解释了为什么您看到创建了两个 int 对象, 这也意味着我的猜测是正确的:解析器首先创建常量,然后才优化字节码以对相同的常量使用相同的对象。

执行此检查的代码必须在 Python/compile.cPython/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;

所以看起来 compiler_add_o 用于为函数/lambda 等插入常量。 compiler_add_o 函数将常量存储到一个 dict 对象中,从这里可以看出,相等的常量将落在同一个槽中,从而在最终字节码中产生一个常量。

关于python - 解释器维护的整数缓存有什么用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54943783/

相关文章:

python - Scrapy只返回了1个项目,我的大脑卡住了

python - 确认 TF2 在训练时使用我的 GPU

python - 奇怪的 Python Bug - 来自 cron 的 Stagger (ID3) 编码错误,但不是来自命令行的错误

java - 存储在 Hibernate 二级缓存中的对象是否保证为 "immutable"?

python - python中的邻近矩阵

asp.net - 如何在 ASP.NET 3.5 中对每个 http 请求进行缓存

java - 如何比较各种缓存框架的速度?

PHP 代码分析器以确定使用的类/扩展

delphi - 任何推荐的 Delphi 代码导航工具?

c# - 生成文件的Visual Studio代码分析