Python 3.5 vs. 3.6使得"map"与理解相比变慢的原因

MSe*_*ert 10 python performance cpython python-3.5 python-3.6

我有时会使用map如果有一个用C语言编写的函数/方法来获得更多的性能.然而,我最近重新审视了一些基准测试,并注意到相对性能(与类似的列表理解相比)在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]
Run Code Online (Sandbox Code Playgroud)

我意识到使用它并不是一个好主意,(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)
Run Code Online (Sandbox Code Playgroud)

虽然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)
Run Code Online (Sandbox Code Playgroud)

我的问题是在这种情况下发生了什么使列表理解更快,map解决方案更慢?我意识到区别并不是那么多,它只是让我很好奇,因为这是我有时(实际上很少)在性能关键代码中使用的"技巧"之一.

AGN*_*zer 5

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

在我最初的回答中,我进行了多次测试,结果表明,map与列表推导相比,两个版本的Python中的测试速度仍然提高了约两倍.然而,一些结果并不是结论性的,所以我进行了一些更多的测试.

首先让我引用一些问题中提出的要点:

"...... [我]注意到相对的表现map(与类似的列表理解相比)在Python 3.5和3.6之间彻底改变了"

你也问:

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

目前还不是很清楚你是否认为地图比Python 3.6中的列表理解慢,或者你的意思是Python 3.6中的地图比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.
Run Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)

让我们首先解决"相同数据"的问题.不幸的是因为你有效地使用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)
Run Code Online (Sandbox Code Playgroud)

其次,让我们使用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
Run Code Online (Sandbox Code Playgroud)

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

对于我不知道的原因,IPython的魔力%timeittimeit包相比增加了一些时间.因此,我将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
Run Code Online (Sandbox Code Playgroud)

请注意第二个测试(使用列表理解f(i))是否明显慢于第三个测试(使用列表理解5 < i),表明f = (5).__lt__5 < 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
Run Code Online (Sandbox Code Playgroud)

请注意,从代码的角度来看,第一次测试(of f(1))是否明显慢于第二次测试(of 5 < 1)进一步支持f = (5).__lt__不同的(或几乎相同)5 < 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
Run Code Online (Sandbox Code Playgroud)

第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
Run Code Online (Sandbox Code Playgroud)

请注意,从代码的角度来看,第一次测试(of f(1))是否明显慢于第二次测试(of 5 < 1)进一步支持f = (5).__lt__不同的(或几乎相同)5 < i.

讨论

我不知道这些时序测试的可靠性如何,并且很难将导致这些时序结果的所有因素分开.然而,我们可以从测试的"第2组"中注意到,唯一显着改变其时序的"个体"测试是测试5 < 1:它在Python 3.6中从0.03,99s下降到0.0268s.这使得Python 3.6中的列表理解测试5 < i比Python 3.5中的类似测试运行得更快.但是,这并不意味着Python 3.6中的列表理解变得更快.

让我们比较相同Python版本中相同函数的列表理解的相对性能map.然后,我们得到在Python 3.5: r(f) = 7.4428/4.6666 = 1.595,r(abs) = 5.665/4.167 = 1.359并在Python 3.6: ,.r(f) = 7.316/4.5997 = 1.591 r(abs) = 6.218/2.743 = 2.267基于这些相对性能,我们可以看到在Python 3.6中,map相对于列表理解性能的性能至少与Python 3.5中的f = (5).__lt__函数相同,并且这个比例甚至abs()在Python 3.6 等函数中得到了改进.

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

  • @MSeifert如果你查看我的测试结果,你会发现`map`的性能已经提高了(从Python 3.5到3.6),增加了100*(4.66655-4.5997)/4.5997 = 1.4%`并且列表理解的性能提高了由'100*(4.9465-4.57)/ 4.57 = 8.2%`.因此,`map`的性能几乎没有变化(在错误中)和list compr.性能有*非常适度*增加几乎使它与`map`(在错误内)的速度相同.如上所述,当测试"增强"(使用`timeit`,相同的数据数组,增加数据大小等)时,我肯定没有看到`map`性能下降20%. (2认同)

AGN*_*zer 0

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

\n\n
>>> import sys\n>>> print(sys.version)\n3.6.2 |Continuum Analytics, Inc.| (default, Jul 20 2017, 13:14:59) \n[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]\n>>> import random\n>>> lst = [random.randint(0, 10) for _ in range(100000)]\n>>> assert list(map((5).__lt__, lst)) == [5 < i for i in lst]\n>>> f = (5).__lt__\n>>> %timeit list(map(f, lst))\n4.63 ms \xc2\xb1 110 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n>>> %timeit [f(i) for i in lst]\n9.17 ms \xc2\xb1 177 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

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

尽管如此,当使用相同的函数时,map它比 Python 3.5 和 3.6 中的列表理解快约 2 倍。

\n\n

编辑(回复@user2357112评论):

\n\n

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

\n\n
timig_map_35 = Timing(list(map(f, lst)))\ntiming_list_35 = Timing([g(i) for i in lst])\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

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

在 Python 3.6 中我得到:

\n\n
43.5 ms \xc2\xb1 1.79 ms per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1 loop each)\n82.2 ms \xc2\xb1 2.39 ms per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1 loop each)\n43.6 ms \xc2\xb1 1.64 ms per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1 loop each)\n23.8 ms \xc2\xb1 1.27 ms per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1 loop each)\n
Run Code Online (Sandbox Code Playgroud)\n\n

在 Python 3.5 中我得到:

\n\n
1 loop, best of 1000: 43.7 ms per loop\n1 loop, best of 1000: 78.9 ms per loop\n1 loop, best of 1000: 46 ms per loop\n1 loop, best of 1000: 26.8 ms per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
%timeit -n1 -r1000 list(map(abs, lst))\n%timeit -n1 -r1000 [abs(i) for i in lst]\n%timeit -n1000000 -r1000 f(1)\n
Run Code Online (Sandbox Code Playgroud)\n\n

在 Python 3.6 中我得到:

\n\n
25.8 ms \xc2\xb1 1.42 ms per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1 loop each)\n67.1 ms \xc2\xb1 2.07 ms per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1 loop each)\n64.7 ns \xc2\xb1 2.22 ns per loop (mean \xc2\xb1 std. dev. of 1000 runs, 1000000 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n\n

在 Python 3.5 中我得到:

\n\n
1 loop, best of 1000: 38.3 ms per loop\n1 loop, best of 1000: 56.4 ms per loop\n1000000 loops, best of 1000: 59.6 ns per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n