解释器维护的整数缓存是什么?


82

深入研究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

但是,如果我在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_FromLong之前/之后添加了一些调试代码PyAST_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_Objects的创建(实际上它的真实执行的ast_for_atom()功能),但他们后来合并。

我觉得很难理解的来源PyAST_CompilePyEval_EvalCode,所以我在这里寻求帮助,如果有一个人给了一个暗示,我会感激?


2
您是否只是想了解Python源代码的工作原理,还是想了解使用Python编写的代码的成功之处?因为用Python编写的代码的结果是“这是一个实现细节,所以永远不要依赖它的发生与否”。
BrenBarn

我不会依赖于实现细节。我很好奇,并尝试闯入源代码。
felix021 2013年


@Blckknght谢谢。我已经知道了这个问题的答案,而且我会做得更多。
felix021

Answers:


103

Python会缓存范围内的整数[-5, 256],因此可以预期该范围内的整数也相同。

您会看到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字节码编译器无法执行大规模优化(如静态类型语言),但是它的功能超出您的想象。这些事情之一是分析文字的用法并避免重复。

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

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

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

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

解析器有两种选择:

  • 在创建整数之前,将文字转换为某些通用基数,然后查看文字是否等效。然后创建一个整数对象。
  • 创建整数对象,然后查看它们是否相等。如果是,则仅保留一个值并将其分配给所有文字,否则,您已经具有要分配的整数。

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

因此,似乎compiler_add_o将其用于为函数/ ​​lambdas等插入常量。该compiler_add_o函数将这些常量存储到dict对象中,并且从此之后,相等的常量将立即落入同一插槽中,从而在最终字节码中生成单个常量。


谢谢。我知道为什么解释器会这样做,而且我之前也测试过字符串,其作用与int和float相同,并且我还使用了editor.parse()打印了语法树,其中显示了两个Const(257)。我只是想知道何时以及如何在源代码中……而且,我在上面所做的测试表明,解释器已经为a和b创建了两个PyInt_Object,因此合并它们实际上没有什么意义(除了节省内存)。
felix021

@ felix021我再次更新了我的答案。我找到了创建两个int的位置,并且知道在哪个文件中进行了优化,即使我仍然找不到处理该问题的确切代码行。
巴库里

非常感谢!我仔细检查了compile.c,调用链是compile_visit_stmt-> VISIT(c,expr,e)-> compile_visit_expr(c,e)-> ADDOP_O(c,LOAD_CONST,e-> v.Num.n,consts) ->编译器addop_o(c,LOAD_CONSTS,c-> u-> u_consts,e-> v.num.n)->编译器_add_o(c,c-> u-> u_consts,e-> v.Num.n)。在compoler_add_o()中,python将尝试将if-not-find-then-set PyTuple(PyIntObject n,PyInt_Type)设置为c-> u-> u_consts的键,并在计算该元组的哈希时,仅将实际int使用了value,因此只有一个PyInt_Object将被插入到u_consts dict中。
felix021

我得到False执行a = 5.0; b = 5.0; print (a is b)与PY2和PY3 Win7上都
zhangxaochen

1
@zhangxaochen您是否在交互式解释器的同一行或不同行中编写了这两个语句?无论如何,不​​同版本的python可能会产生不同的行为。在我的机器上确实会导致True(现在重新检查)。优化是不可靠的,因为它们只是实现的细节,因此不会使我想在回答中提出的观点无效。还compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_consts显示只有一个5.0常量(在Linux上的python3.3中)。
Bakuriu 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.