【问题标题】:Python 3.5 vs. 3.6 what made "map" slower compared to comprehensions与理解相比,Python 3.5 与 3.6 使“地图”变慢的原因
【发布时间】:2018-01-17 23:48:09
【问题描述】:

如果有一个用 C 语言编写的函数/方法来获得额外的性能,我有时会使用 map。然而,我最近重新审视了一些基准测试,并注意到 Python 3.5 和 3.6 之间的相对性能(与类似的列表理解相比)发生了巨大变化。

这不是实际代码,只是说明差异的最小示例:

import random

lst = [random.randint(0, 10) for _ in range(100000)]
assert list(map((5).__lt__, lst)) == [5 < i for i in lst]
%timeit list(map((5).__lt__, lst))
%timeit [5 < i for i in lst]

我意识到使用(5).__lt__ 不是一个好主意,但我现在想不出一个有用的例子。

Python-3.5 的时序支持map 方法:

15.1 ms ± 5.64 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
16.7 ms ± 35.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

虽然 Python-3.6 的时序实际上表明理解更快:

17.9 ms ± 755 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
14.3 ms ± 128 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

我的问题是在这种情况下发生了什么使列表理解更快而map 解决方案更慢?我意识到差异并没有那么大,只是让我感到好奇,因为这是我有时(实际上很少)在性能关键代码中使用的“技巧”之一。

【问题讨论】:

  • 嗯,也许这取决于功能?例如,str 似乎仍然使用map 更快,所以timeit.timeit("list(map(str, range(100000)))", number=1000) 给了我18.98376033702516timeit.timeit("[str(i) for i in range(100000)]", number=1000) 给了我24.40201253897976
  • 该模式适用于intfloat ...而且这几乎耗尽了我对list(map(...)) 的用例超过列表理解;)
  • map 减速的可能候选者包括对 map.__next__ 的更改,当映射函数为 (5).__lt__ 时,该更改不会得到回报。 (3.5 version, 3.6 version) 差异很小,以至于很难找到真正的原因并验证这是原因。
  • @user2357112 你不能从这个测试中推断出“地图变慢了”。你可以说“这个特定的测试与这个特定的列表理解相比变慢了”,这可能是由很多原因造成的,其中一个原因是map 变慢了,但同样可能的是列表理解通常变得更快。由于两者都做非常不同的事情,因此您不能仅将差异归因于map。 – 事实上,我自己的测试表明,总体而言执行时间变快了(至少对我而言),而且 OP 的列表推导比其他推导快得多。
  • @poke:我并不是说map 本身一定变慢了。语言模棱两可;我提到“map 减速”并不是为了声称map 本身就是罪魁祸首,就像我在谈论“我的电脑速度变慢”一样,我不会声称我的物理硬件必须是负责任的。 (使用map 的 sn-p 在 MSeifert 的 Python 3.6 时序中绝对运行速度确实较慢,因此仅在列表理解改进方面的解释并不令人满意。)

标签: python performance python-3.5 python-3.6 cpython


【解决方案1】:

我认为公平的比较包括在 Python 3.5 和 3.6 中使用相同的函数和相同的测试条件,以及将 map 与所选 Python 版本中的列表理解进行比较。

在我最初的回答中,我进行了多项测试,结果表明,与列表理解相比,map 在两个 Python 版本中的速度仍然快了大约两倍。但是有些结果还没有定论,所以我进行了更多测试。

首先让我引用您在问题中陈述的一些观点:

"... [我] 注意到 [map] 的相对性能(与类似的列表理解相比)急剧在 Python 3.5 和 3.6 之间发生了变化

你还问:

“我的问题是,在这种情况下发生了什么使列表理解更快而地图解决方案更慢?”

你的意思是 map 比 Python 3.6 中的列表理解慢,还是你的意思是 Python 3.6 中的 map 比 3.5 慢并且列表理解的性能有所提高,目前还不是很清楚(尽管不一定达到跳动的水平map)。

根据我在第一次回答这个问题后进行的更广泛的测试,我想我知道发生了什么。

但是,首先让我们为“公平”比较创造条件。为此,我们需要:

  1. 使用相同函数比较map在不同Python版本中的性能;

  2. 比较map 的性能与使用相同功能的相同版本中的列表理解;

  3. 对相同数据运行测试;

  4. 尽量减少计时函数的贡献。

这是关于我的系统的版本信息:

Python 3.5.3 |Continuum Analytics, Inc.| (default, Mar  6 2017, 12:15:08) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin
IPython 5.3.0 -- An enhanced Interactive Python.

Python 3.6.2 |Continuum Analytics, Inc.| (default, Jul 20 2017, 13:14:59) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.

让我们首先解决“相同数据”的问题。不幸的是,因为您有效地使用了seed(None),所以每个数据集lst 在两个Python 版本中都不同。这可能导致两个 Python 版本的性能差异。一种解决方法是设置,例如,random.seed(0)(或类似的东西)。我选择创建一次列表并使用numpy.save() 保存它,然后在每个版本中加载它。这一点尤其重要,因为我选择稍微修改您的测试(“循环”和“重复”的数量),并且我已将数据集的长度增加到 100,000,000:

import numpy as np
import random
lst = [random.randint(0, 10) for _ in range(100000000)]
np.save('lst', lst, allow_pickle=False)

其次,让我们使用timeit 模块而不是IPython 的魔术命令%timeit。这样做的原因来自于 Python 3.5 中执行的以下测试:

In [11]: f = (5).__lt__
In [12]: %timeit -n1 -r20 [f(i) for i in lst]
1 loop, best of 20: 9.01 s per loop

将此与同一版本 Python 中 timeit 的结果进行比较:

>>> t = timeit.repeat('[f(i) for i in lst]', setup="f = (5).__lt__;
... import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, 
... number=1); print(min(t), max(t), np.mean(t), np.std(t))
7.442819457995938 7.703615028003696 7.5105415405 0.0550515642854

由于我不知道的原因,与 timeit 包相比,IPython 的魔法 %timeit 增加了一些时间。因此,我将在我的测试中专门使用timeit

注意:在接下来的讨论中,我将只使用最短时间 (min(t))。

Python 3.5.3 中的测试:

第 1 组:地图和列表理解测试

>>> import numpy as np
>>> import timeit

>>> t = timeit.repeat('list(map(f, lst))', setup="f = (5).__lt__; import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
4.666553302988177 4.811194089008495 4.72791638025 0.041115884397

>>> t = timeit.repeat('[f(i) for i in lst]', setup="f = (5).__lt__; import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
7.442819457995938 7.703615028003696 7.5105415405 0.0550515642854

>>> t = timeit.repeat('[5 < i for i in lst]', setup="import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
4.94656751700677 5.07807950800634 5.00670203845 0.0340474956945

>>> t = timeit.repeat('list(map(abs, lst))', setup="import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
4.167273573024431 4.320013975986512 4.2408865186 0.0378852782878

>>> t = timeit.repeat('[abs(i) for i in lst]', setup="import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
5.664627838006709 5.837686392012984 5.71560354655 0.0456700607748

注意第二个测试(使用f(i) 的列表理解)比第三个测试(使用5 &lt; i 的列表理解)慢得多,这表明从代码角度来看f = (5).__lt__5 &lt; i 不同(或几乎相同) .

第 2 组:“个人”功能测试

>>> t = timeit.repeat('f(1)', setup="f = (5).__lt__", repeat=20, number=1000000); print(min(t), max(t), np.mean(t), np.std(t))
0.052280781004810706 0.05500587198184803 0.0531139718529 0.000877649561967

>>> t = timeit.repeat('5 < 1', repeat=20, number=1000000); print(min(t), max(t), np.mean(t), np.std(t))
0.030931947025237605 0.033691533986711875 0.0314959864045 0.000633274658428

>>> t = timeit.repeat('abs(1)', repeat=20, number=1000000); print(min(t), max(t), np.mean(t), np.std(t))
0.04685414198320359 0.05405496899038553 0.0483296330043 0.00162837880358

请注意,第一次测试(f(1))比第二次测试(5 &lt; 1)慢得多,从代码角度进一步支持f = (5).__lt__5 &lt; i 不同(或几乎相同)。

Python 3.6.2 中的测试:

第 1 组:地图和列表理解测试

>>> import numpy as np
>>> import timeit

>>> t = timeit.repeat('list(map(f, lst))', setup="f = (5).__lt__; import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
4.599696700985078 4.743880658003036 4.6631793691 0.0425774678203

>>> t = timeit.repeat('[f(i) for i in lst]', setup="f = (5).__lt__; import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
7.316072431014618 7.572676292009419 7.3837024617 0.0574811241553

>>> t = timeit.repeat('[5 < i for i in lst]', setup="import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
4.570452399988426 4.679144663008628 4.61264215875 0.0265541828693

>>> t = timeit.repeat('list(map(abs, lst))', setup="import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
2.742673939006636 2.8282236389932223 2.78504617405 0.0260357089928

>>> t = timeit.repeat('[abs(i) for i in lst]', setup="import numpy; lst = numpy.load('lst.npy').tolist()", repeat=20, number=1); print(min(t), max(t), np.mean(t), np.std(t))
6.2177103200228885 6.428813881997485 6.28722427145 0.0493010620999

第 2 组:“个人”功能测试

>>> t = timeit.repeat('f(1)', setup="f = (5).__lt__", repeat=20, number=1000000); print(min(t), max(t), np.mean(t), np.std(t))
0.051936342992121354 0.05764096099301241 0.0532974587506 0.00117079475737

>>> t = timeit.repeat('5 < 1', repeat=20, number=1000000); print(min(t), max(t), np.mean(t), np.std(t))
0.02675032999832183 0.032919151999522 0.0285137565021 0.00156522182488

>>> t = timeit.repeat('abs(1)', repeat=20, number=1000000); print(min(t), max(t), np.mean(t), np.std(t))
0.047831349016632885 0.0531779529992491 0.0482893927969 0.00112825297875

请注意,第一次测试(f(1))比第二次测试(5 &lt; 1)慢得多,进一步支持从代码角度来看f = (5).__lt__5 &lt; i 不同(或几乎相同)。

讨论

我不知道这些时序测试的可靠性如何,而且也很难将导致这些时序结果的所有因素分开。然而,我们可以从“第 2 组”测试中注意到,唯一显着改变其时间的“个人”测试是 5 &lt; 1 的测试:它在 Python 3.6 中从 Python 3.5 中的 0.0309 秒下降到 0.0268 秒。这使得 Python 3.6 中使用 5 &lt; i 的列表理解测试比 Python 3.5 中的类似测试运行得更快。然而,这并不意味着列表解析在 Python 3.6 中变得更快。

让我们将map相对性能与相同Python 版本中相同函数的列表理解进行比较。然后我们进入 Python 3.5:r(f) = 7.4428/4.6666 = 1.595r(abs) = 5.665/4.167 = 1.359 和 Python 3.6:r(f) = 7.316/4.5997 = 1.591r(abs) = 6.218/2.743 = 2.267。基于这些相对性能,我们可以看到,在 Python 3.6 中,map 相对于列表理解的性能至少与 Python 3.5 中 f = (5).__lt__ 函数的性能相同,并且对于这样的函数,这个比率甚至有所提高在 Python 3.6 中为 abs()

无论如何,我相信没有证据表明 Python 3.6 中的列表理解变得更快,无论是相对还是绝对意义上的。唯一的性能改进是 [5 &lt; i for i in lst] 测试,但这是因为 5 &lt; i 本身在 Python 3.6 中变得更快,而不是因为列表理解本身更快。

【讨论】:

  • 您为一般情况提出了要点(非常感谢!)。但是我仍然不知道是什么原因使map 慢了~20% 而理解速度快了~20%在我的情况下。请注意,比较故意不“公平”(除了种子问题 - 我忽略了这一点 - 但如果我使用 pickle 在两个版本中使用相同的列表,它会给出相同的结果)。它将map 解决问题的“最快”方法与列表理解解决问题的最快方法进行了比较。 :)
  • @MSeifert 如果您查看我的测试结果,您会看到map 的性能提高了100*(4.66655-4.5997)/4.5997 = 1.4%(从Python 3.5 到3.6),列表理解的性能提高了@987654376 @。因此,map 的性能几乎没有变化(在错误范围内)并且列表比较。性能有非常适度的提升,几乎达到与map 相同的速度(在错误范围内)。当测试被“增强”(使用timeit、相同的数据数组、增加数据大小等)时,我当然没有看到map 的性能下降20%。
  • @user8371915 谢谢!
【解决方案2】:

我认为公平的比较将涉及使用相同的功能。在您的示例中,当比较公平时,map 仍然获胜:

>>> import sys
>>> print(sys.version)
3.6.2 |Continuum Analytics, Inc.| (default, Jul 20 2017, 13:14:59) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]
>>> import random
>>> lst = [random.randint(0, 10) for _ in range(100000)]
>>> assert list(map((5).__lt__, lst)) == [5 < i for i in lst]
>>> f = (5).__lt__
>>> %timeit list(map(f, lst))
4.63 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit [f(i) for i in lst]
9.17 ms ± 177 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

虽然在 Python 3.5(至少在我的系统上)map 比 Python 3.6 更快,但列表理解也是如此:

>>> print(sys.version)
3.5.3 |Continuum Analytics, Inc.| (default, Mar  6 2017, 12:15:08) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]
>>> %timeit list(map(f, lst))
100 loops, best of 3: 4.36 ms per loop
>>> %timeit [f(i) for i in lst]
100 loops, best of 3: 8.12 ms per loop

不过,当使用相同的函数时,map 在 Python 3.5 和 3.6 中都比列表解析快约 2 倍。

编辑(回复@user2357112 cmets):

我认为执行“公平”比较对于回答 OP 的问题很重要:“我的问题是,在这种情况下发生了什么使列表理解更快而地图解决方案更慢?” (最后一段)。然而,在第一段中,@MSeifert 说:“... [我] 注意到 Python 3.5 和 3.6 之间的相对性能(与类似的列表理解相比)发生了巨大变化” 也就是说,比较是在maplist comprehension 之间进行的。然而,@MSeifert 测试设置如下:

timig_map_35 = Timing(list(map(f, lst)))
timing_list_35 = Timing([g(i) for i in lst])

这种测试很难找到时间差异的原因:是因为列表理解在 3.6 中变快了,还是因为 map 在 3.6 中变慢了,或者 f(i) 在 3.6 中变慢了,或者 g(i) 在 3.6 中变快了。 ..

因此,我建议引入f = (5).__lt__,并在map 和列表理解测试中使用相同的功能。我还修改了@MSeifert 测试,增加了列表中的元素数量并减少了timeit 中“循环”的数量:

import random
lst = [random.randint(0, 10) for _ in range(1000000)] # 10x more elements
f = (5).__lt__
%timeit -n1 -r1000 list(map(f, lst)) # f = (5).__lt__
%timeit -n1 -r1000 [f(i) for i in lst] # f(i) = (5).__lt__(i)
%timeit -n1 -r1000 [5 < i for i in lst] # g(i) = 5 < i
%timeit -n1 -r1000 [1 for _ in lst] # h(i) = 1

在 Python 3.6 中我得到:

43.5 ms ± 1.79 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
82.2 ms ± 2.39 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
43.6 ms ± 1.64 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
23.8 ms ± 1.27 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)

在 Python 3.5 中,我得到:

1 loop, best of 1000: 43.7 ms per loop
1 loop, best of 1000: 78.9 ms per loop
1 loop, best of 1000: 46 ms per loop
1 loop, best of 1000: 26.8 ms per loop

在我看来,这表明列表理解在 3.6 中比在 3.5 中稍快,除非使用 f。因此,很难断定是 map 在 Python 3.6 中较慢,或者是上面的第一个 timeit 较慢,因为对 f 的调用较慢。因此我又进行了两次测试:

%timeit -n1 -r1000 list(map(abs, lst))
%timeit -n1 -r1000 [abs(i) for i in lst]
%timeit -n1000000 -r1000 f(1)

在 Python 3.6 中我得到:

25.8 ms ± 1.42 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
67.1 ms ± 2.07 ms per loop (mean ± std. dev. of 1000 runs, 1 loop each)
64.7 ns ± 2.22 ns per loop (mean ± std. dev. of 1000 runs, 1000000 loops each)

在 Python 3.5 中,我得到:

1 loop, best of 1000: 38.3 ms per loop
1 loop, best of 1000: 56.4 ms per loop
1000000 loops, best of 1000: 59.6 ns per loop

这表明map 可以 明显快 对于某些功能:具体而言,abs(x) map 与“列表理解”的相对性能" 在 Python 3.6 中是 67.1/25.8 = 2.60 而在 Python 3.5 中是 56.4/38.3 = 1.47。因此,有趣的是知道为什么@MSeifert 测试显示map 在 Python 3.6 中速度较慢。我上面的最后一个测试显示了f(1)“单独”的计时测试。我不确定这个测试的有效性(不幸的是)——我想避免使用map[for] 来消除一个变量——但它表明在 Python 3.6 中f = (5).__lt__ 变得比在 Python 3.5 中慢。因此,我得出结论,它是函数f ((5).__lt__) 的特定形式,它的评估速度变慢了,而不是map 函数。我知道最后一次“单独”测试可能是一个糟糕的测试,但是,map 在与abs 一起使用时非常快(相对或绝对)这一事实表明问题出在f 而不是@987654360 @。

注意:Python 3.5 使用 IPython 5.3.0,Python 3.6 使用 IPython 6.1.0。

【讨论】:

  • 问题是为什么性能比较在 3.5 和 3.6 之间发生了变化。进行比较是否“公平”并不重要。
  • @user2357112 没有。@MSeifert 说:"... [I] 注意到 相对性能(与类似的列表理解相比)急剧在 Python 3.5 和 3.6 之间发生了变化我的实验表明,使用相同的函数时(=公平比较)相对性能map 和列表Python 3.5 和 3.6 的理解没有改变(至少没有剧烈),这就是 OP 所说的改变。
  • 虽然我同意差异并不大(并且不同意您对公平的判断),但这并不能回答问题。
  • @user2357112 我添加了额外的测试,我相信这些测试表明,与 Python 3.5 相比,Python 3.6 中f = (5).__lt__ 的评估速度较慢。
猜你喜欢
  • 2018-10-27
  • 1970-01-01
  • 1970-01-01
  • 2019-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多