[python] 为什么[]比list()更快?



Answers

list()需要全局查找和函数调用,但是[]编译为单个指令。 看到:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None
Question

我最近比较了[]list()的处理速度,并惊讶地发现[]list()运行速度快三倍以上。 我用{}dict()进行了相同的测试,结果几乎相同: []{}都花费了大约0.128秒/百万的周期,而list()dict()花费了大约0.428秒/百万的周期。

为什么是这样? Do []{} (也可能是()'' )会立即传回一些空的库存文字的副本,而其明确命名的对应文件( list()dict()tuple()str() )完全去创建一个对象,不管他们是否真的有元素?

我不知道这两种方法有什么不同,但我很想知道。 我无法在文档或搜索结果中找到答案,搜索空括号的结果证明比我预期的更成问题。

我通过调用timeit.timeit("[]")timeit.timeit("list()")timeit.timeit("{}")timeit.timeit("dict()")获得我的计时结果,分别比较列表和词典。 我正在运行Python 2.7.9。

我最近发现“ 为什么如果True比1更慢? ”将if Trueif 1的性能进行比较,并似乎涉及类似的文字与全局情景; 也许这也值得考虑。




这里的答案很好,完全覆盖了这个问题。 对于那些感兴趣的人,我会从字节码中再下一步。 我正在使用最新的CPython回购; 旧版本在这方面表现类似,但可能会有轻微的变化。

这里是每个执行的BUILD_LIST[] CALL_FUNCTIONlist() CALL_FUNCTION

BUILD_LIST指令:

你应该看看恐怖:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

非常令人费解,我知道。 这是多么简单:

  • 使用PyList_New创建一个新列表(这主要是为新列表对象分配内存), oparg表示堆栈中参数的数量。 开门见山。
  • 检查if (list==NULL)没有出错。
  • 使用PyList_SET_ITEM (宏)添加位于堆栈中的任何参数(在我们的情况下,这不会被执行)。

难怪它很快! 它是为创建新列表而自定义的,没有别的:-)

CALL_FUNCTION指令:

这是您查看处理CALL_FUNCTION的代码时看到的第一件事情:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

看起来很无害,对吧? 嗯,不,不幸的是, call_function并不是一个简单的家伙,他会立即调用这个函数,但它不能。 相反,它从堆栈中抓取对象,抓取堆栈的所有参数,然后根据对象的类型进行切换; 它是一个:

我们正在调用list类型,传递给call_function的参数是PyList_Type 。 CPython现在必须调用一个通用函数来处理任何名为_PyObject_FastCallKeywords可调用对象,更多的函数调用。

这个函数再次对某些函数类型(我不明白为什么)进行检查,然后在需要时为kwargs创建一个字典后,继续调用_PyObject_FastCallDict

_PyObject_FastCallDict终于把我们_PyObject_FastCallDict了某个地方! 执行更多的检查后,它会我们传入的type typetp_call ,即抓取type.tp_call 。 然后继续使用_PyStack_AsTuple传入的参数创建一个元组,最后可以调用一个调用

tp_call ,它匹配type.__call__接管并最终创建列表对象。 它调用与__new__相对应的列表__new__ ,并使用PyType_GenericAlloc分配内存: 这实际上是PyList_New最终捕获的部分 。 以前的所有内容都是以通用方式处理对象所必需的。

最后, type_call调用list.__init__并用任何可用的参数初始化列表,然后我们继续返回到我们来的方式。 :-)

最后,记住LOAD_NAME ,这是另一个在这里做出贡献的人。

很容易看到,在处理我们的输入时,Python通常需要跳过这些循环才能真正找到合适的C函数来完成这项工作。 它没有立即称它的风险,因为它是动态的,有人可能会掩盖list并且男孩会做很多人 ),并且必须采取另一条路径。

这是list()失去很多的地方:探索Python需要做的是找出它应该做什么。

另一方面,文字语法意味着一件事; 它不能被改变,并且总是以预定的方式表现出来。

脚注:所有功能名称都可能会从一个版本更改为另一个版本。 这个观点仍然存在,很可能会在未来的版本中出现,这是动态查找会降低速度。






Related