在使用指定的默认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
中也存在类似的结果。
这就是我被困住的地方,无法弄清楚在哪里看。这可能仅仅是第二种情况下累积额外呼叫的开销,但同样,我并不确定。 有谁知道这背后可能是什么原因?
答案 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)
可能只是导致整体放缓的因素组合。
当Python看到CALL_FUNCTION
参数时,它会调用call_function
,正如您已经指出的那样。在查看了一些if
条款后,发出的电话是x = do_call(func, pp_stack, na, nk);
。请注意nk
此处包含关键字参数的总计数(因此在enumerate -> kw=1
的情况下)。
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_kwdict
是NULL
(查看对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
子句中执行的操作并不是很繁重,但确实会影响所需的总时间。
另外:
你还需要考虑两个额外的字节代码命令,它们可能只是两个,但它们会增加所花费的总时间,因为我们的定时非常快(在范围内) ns
)。
同样,从整体角度来看,这是微不足道的,但是,使用kws
解析一个电话需要像以前一样多一点时间。
<强>最后:强>
我可能会遗漏一些东西,但总体而言,这些是在创建指定了start
的新枚举对象时会产生开销的一些因素。