地图与星图的性能?

Posted

技术标签:

【中文标题】地图与星图的性能?【英文标题】:Performance of map vs starmap? 【发布时间】:2017-09-12 08:53:40 【问题描述】:

我试图对两个序列进行纯python(没有外部依赖项)逐元素比较。我的第一个解决方案是:

list(map(operator.eq, seq1, seq2))

然后我从itertools 中找到了starmap 函数,看起来和我很相似。但在最坏的情况下,它在我的电脑上的速度提高了 37%。由于这对我来说并不明显,我测量了从生成器中检索 1 个元素所需的时间(不知道这种方式是否正确):

from operator import eq
from itertools import starmap

seq1 = [1,2,3]*10000
seq2 = [1,2,3]*10000
seq2[-1] = 5

gen1 = map(eq, seq1, seq2))
gen2 = starmap(eq, zip(seq1, seq2))

%timeit -n1000 -r10 next(gen1)
%timeit -n1000 -r10 next(gen2)

271 ns ± 1.26 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each)
208 ns ± 1.72 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each)

在检索元素时,第二种解决方案的性能提高了 24%。之后,它们都为list 产生相同的结果。但是从某个地方我们获得了额外的 13% 时间:

%timeit list(map(eq, seq1, seq2))
%timeit list(starmap(eq, zip(seq1, seq2)))

5.24 ms ± 29.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.34 ms ± 84.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

我不知道如何更深入地分析这种嵌套代码?所以我的问题是为什么第一个生成器的检索速度如此之快,并且我们在list 函数中获得了额外的 13%?

编辑: 我的第一个意图是执行逐元素比较而不是all,因此all 函数被替换为list。此替换不影响时序比。

Windows 10(64 位)上的 CPython 3.6.2

【问题讨论】:

为什么不直接使用seq1 == seq2 @Błotosmętek 感谢您的指正!我的第一个意图是元素比较而不是all,这在我的问题中并不明显:) 真的,如果你用list 代替all,时间顺序将是相同的。 什么 Python 版本?这是 CPython 吗? @MSeifert 是的,Windows 10(64 位)上的 CPython 3.6.2 【参考方案1】:

有几个因素会导致(共同)观察到的性能差异:

如果 zip 在下一次调用 __next__ 时引用计数为 1,则 zip 会重新使用返回的 tuplemap 构建一个 new tuple,每次调用 __next__ 时都会传递给“映射函数”。实际上,它可能不会从头开始创建新的元组,因为 Python 为未使用的元组维护了一个存储。但在这种情况下,map 必须找到一个未使用的大小合适的元组。 starmap 检查可迭代对象中的下一项是否为 tuple 类型,如果是,则将其传递。 在 C 代码中使用 PyObject_Call 调用 C 函数不会创建传递给被调用者的新元组。

所以starmapzip 只会一遍又一遍地使用一个传递给operator.eq 的元组,从而极大地减少函数调用开销。另一方面,map 将在每次调用 operator.eq 时创建一个新元组(或从 CPython 3.6 开始填充 C 数组)。所以实际上速度差异只是元组创建开销。

我将提供一些可用于验证这一点的 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)

是的,tuples 并不是真正不可变的,一个简单的Py_DECREF 就足以“欺骗”zip 相信没有其他人持有对返回元组的引用!

至于“tuple-pass-thru”:

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

所以有很多“巧合”导致starmapzip 在被调用函数也是C 函数时比使用多个参数调用map 更快...

【讨论】:

【参考方案2】:

我注意到的一个区别是map 如何从可迭代对象中检索项目。 mapzip 都从每个传递的迭代器中创建一个迭代器元组。现在 zip 在内部维护一个 result tuple ,每次调用 next 时都会填充它,另一方面,map creates a new array* 每次调用 next 并释放它。


*正如 MSeifert 所指出的,直到 3.5.4 map_next 每次都用于分配一个新的 Python 元组。这在 3.6 中发生了变化,直到使用了 5 个可迭代的 C 堆栈,并且使用了大于该堆的任何东西。相关 PR:Issue #27809: map_next() uses fast call 和 Add _PY_FASTCALL_SMALL_STACK constant |问题:https://bugs.python.org/issue27809

【讨论】:

假设这是 3.6,请注意 3.5.4 中的代码看起来不同。 :) @MSeifert 我想知道 3.5.4 实现与 3.6.2 相比有多少慢/快。 (由于堆与 C-Stack 访问,在 5 次迭代之前应该很慢) 是的,你是对的。我的“研究”让我得出了同样的结论。

以上是关于地图与星图的性能?的主要内容,如果未能解决你的问题,请参考以下文章

奥维互动地图下载|奥维互动地图下载电脑版

GDAL库学习笔记:无缝拼接Google卫星图

百度地图小知识

搜狗地图下载|搜狗地图app下载

用C#编写百度地图Android手机应用程序(第2讲)

mapbox中文版怎么下载地图