为什么在未指定关键字start时枚举执行速度较慢?

时间:2016-01-08 12:14:20

标签: python python-2.7 python-3.x enumerate python-internals

在使用指定的默认start参数计时enumerate时,我注意到以下奇怪的行为:

In [23]: %timeit enumerate([1, 2, 3, 4])
The slowest run took 7.18 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 511 ns per loop

In [24]: %timeit enumerate([1, 2, 3, 4], start=0)
The slowest run took 12.45 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 1.22 µs per loop

因此,对于指定start的情况,大约减少2倍。

为每个案例发布的字节代码并没有真正指出任何会导致速度显着差异的因素。例如,在使用dis.dis检查不同的调用之后,发出的其他命令是:

18 LOAD_CONST               5 ('start')
21 LOAD_CONST               6 (0)

这些以及CALL_FUNCTION有1个关键字是唯一的区别。

我尝试使用CPython跟踪gdb s ceval中的调用,并且两者似乎都在call_function使用do_call而不是其他优化我可以检测到。

现在,我理解enumerate只是创建一个枚举迭代器,所以我们在这里处理对象创建(对吧?)。我查看 Objects/enumobject.c ,如果指定了start,则会发现任何差异。 (我相信)唯一不同的是start != NULL时发生以下情况:

if (start != NULL) {
    start = PyNumber_Index(start);
    if (start == NULL) {
        Py_DECREF(en);
        return NULL;
    }
    assert(PyInt_Check(start) || PyLong_Check(start));
    en->en_index = PyInt_AsSsize_t(start);
    if (en->en_index == -1 && PyErr_Occurred()) {
        PyErr_Clear();
        en->en_index = PY_SSIZE_T_MAX;
        en->en_longindex = start;
    } else {
        en->en_longindex = NULL;
        Py_DECREF(start);
    }

这看起来不会引起2倍减速。 (我想,不确定。)

之前的代码段已在Python 3.5上执行,但2.x中也存在类似的结果。

这就是我被困住的地方,无法弄清楚在哪里看。这可能仅仅是第二种情况下累积额外呼叫的开销,但同样,我并不确定。 有谁知道这背后可能是什么原因?

2 个答案:

答案 0 :(得分:6)

一个原因可能是因为在下一部分中指定了一个开头时调用PyNumber_Index

if (start != NULL) {
    start = PyNumber_Index(start);

如果您查看abstract.c模块中的PyNumber_Index功能,您会在功能的顶层看到以下评论:

/* Return a Python int from the object item.
   Raise TypeError if the result is not an int
   or if the object cannot be interpreted as an index.
*/

因此,此函数必须检查对象是否不能被解释为索引并返回相对错误。如果仔细查看源代码,您将看到所有这些检查和引用,特别是在下面的部分中,为了检查索引类型,必须进行嵌套结构取消引用:

result = item->ob_type->tp_as_number->nb_index(item);
if (result &&
     !PyInt_Check(result) && !PyLong_Check(result)) {
                         ...

需要花费很多时间来检查并返回一个愿望结果。

但正如@ user2357112所提到的,另一个也是最重要的原因是因为python关键字参数匹配。

如果你计时没有关键字参数,你会看到差异时间会减少大约2倍的时间:

~$ python -m timeit "enumerate([1, 2, 3, 4])"
1000000 loops, best of 3: 0.251 usec per loop
~$ python -m timeit "enumerate([1, 2, 3, 4],start=0)"
1000000 loops, best of 3: 0.431 usec per loop
~$ python -m timeit "enumerate([1, 2, 3, 4],0)"
1000000 loops, best of 3: 0.275 usec per loop

与位置参数的区别在于:

>>> 0.251 - 0.275
-0.024

这似乎是因为PyNumber_Index

答案 1 :(得分:1)

可能只是导致整体放缓的因素组合。

关键字args:

当Python看到CALL_FUNCTION参数时,它会调用call_function,正如您已经指出的那样。在查看了一些if条款后,发出的电话是x = do_call(func, pp_stack, na, nk);。请注意nk此处包含关键字参数的总计数(因此在enumerate -> kw=1的情况下)。

do_call中,您会看到以下if条款:

if (nk > 0) {
    kwdict = update_keyword_args(NULL, nk, pp_stack, func);
    if (kwdict == NULL)
        goto call_fail;
}

如果关键字参数的数量不为零(nk > 0),请致电update_keyword_args。 现在,update_keyword_args执行您期望的操作,if orig_kwdictNULL(查看对update_keyword_args的调用)创建新词典:

if (orig_kwdict == NULL)
    kwdict = PyDict_New();

然后使用值堆栈中存在的所有值填充字典:

while (--nk >= 0) {
// copy from stack

这些可能对整体延迟有重大影响。

创建enum对象:

关于enum_new你是对的,如果使用enumerate([1, 2, 3, 4], start=0)进行调用,start内的变量enum_new会有一个值,因此为!= NULL。因此,if子句将计算为True,其中的代码将执行,从而为调用增加时间。

if子句中执行的操作并不是很繁重,但确实会影响所需的总时间

另外:

  1. 你还需要考虑两个额外的字节代码命令,它们可能只是两个,但它们会增加所花费的总时间,因为我们的定时非常快(在范围内) ns)。

  2. 同样,从整体角度来看,这是微不足道的,但是,使用kws解析一个电话需要像以前一样多一点时间。

  3. <强>最后:

    我可能会遗漏一些东西,但总体而言,这些是在创建指定了start的新枚举对象时会产生开销的一些因素。