有几个因素(共同)导致观察到的性能差异:
-
zip
重新使用返回的tuple
如果它的引用计数为 1,则下一个__next__
已拨打电话。
-
map
建立一个new tuple
每次都会传递给“映射函数”__next__
已拨打电话。实际上,它可能不会从头开始创建新的元组,因为 Python 为未使用的元组维护了一个存储空间。但在那种情况下map
必须找到一个未使用的大小合适的元组。
-
starmap
检查可迭代中的下一项是否属于类型tuple
如果是这样,它就会将其传递下去。
- 从 C 代码中调用 C 函数
PyObject_Call
不会创建传递给被调用者的新元组。
So starmap
with zip
只会一遍又一遍地使用一个传递给的元组operator.eq
从而极大地减少了函数调用的开销。map
另一方面,每次都会创建一个新元组(或从 CPython 3.6 开始填充 C 数组)operator.eq
叫做。所以实际上速度差异只是元组创建开销。
我将提供一些可用于验证这一点的 Cython 代码,而不是链接到源代码:
In [1]: %load_ext cython
In [2]: %%cython
...:
...: from cpython.ref cimport Py_DECREF
...:
...: cpdef func(zipper):
...: a = next(zipper)
...: print('a', a)
...: Py_DECREF(a)
...: b = next(zipper)
...: print('a', a)
In [3]: func(zip([1, 2], [1, 2]))
a (1, 1)
a (2, 2)
Yes, tuple
s 并不是真正不可变的,一个简单的Py_DECREF
就足以“欺骗”zip
相信没有其他人拥有对返回元组的引用!
至于“元组传递”:
In [4]: %%cython
...:
...: def func_inner(*args):
...: print(id(args))
...:
...: def func(*args):
...: print(id(args))
...: func_inner(*args)
In [5]: func(1, 2)
1404350461320
1404350461320
因此元组被直接传递(只是因为它们被定义为 C 函数!)这对于纯 Python 函数不会发生:
In [6]: def func_inner(*args):
...: print(id(args))
...:
...: def func(*args):
...: print(id(args))
...: func_inner(*args)
...:
In [7]: func(1, 2)
1404350436488
1404352833800
请注意,如果被调用的函数不是 C 函数,即使是从 C 函数调用,也不会发生这种情况:
In [8]: %%cython
...:
...: def func_inner_c(*args):
...: print(id(args))
...:
...: def func(inner, *args):
...: print(id(args))
...: inner(*args)
...:
In [9]: def func_inner_py(*args):
...: print(id(args))
...:
...:
In [10]: func(func_inner_py, 1, 2)
1404350471944
1404353010184
In [11]: func(func_inner_c, 1, 2)
1404344354824
1404344354824
所以,有很多“巧合”导致了这一点:starmap
with zip
比打电话更快map
当被调用函数也是 C 函数时具有多个参数...