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

时间:2017-08-09 22:37:29

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

如果有一个用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解决方案更慢?我意识到差别并不大,它让我很好奇,因为这是我有时(实际上很少)在性能关键代码中使用的“技巧”之一。

2 个答案:

答案 0 :(得分:5)

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

在我最初的回答中,我进行了多次测试,结果表明,与列表推导相比,两个版本的Python中map仍然比两倍快。然而,一些结果并不是决定性的,所以我进行了一些测试。

首先让我引用一些问题所述的要点:

  

&#34; ... [我]注意到[3.5]之间[{1}}]的相对表现(与类似的列表理解相比)彻底发生了变化3.6&#34;

你也问:

  

&#34;我的问题是在这种情况下发生了什么使列表理解更快并且地图解决方案更慢?&#34;

目前还不是很清楚你是否认为地图比Python 3.6中的列表理解慢,或者你的意思是Python 3.6中的地图比3.5中慢,而且列表理解的性能有所提高(尽管不一定是击败map)的程度。

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

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

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

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

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

  4. 最大限度地减少计时功能的贡献。

  5. 以下是有关我的系统的版本信息:

    map

    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.
    

    让我们首先解决&#34;相同数据&#34;的问题。不幸的是,因为您实际上正在使用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)在两个版本的Python中都是不同的。这可能会导致两个Python版本的性能差异。一种解决方法是设置,例如lst(或类似的东西)。我选择创建一次列表并使用random.seed(0)保存,然后在每个版本中加载它。这一点尤其重要,因为我选择稍微修改您的测试(&#34;循环&#34;&#34;重复&#34;)并且我已将数据集的长度增加到100,000,000:

    numpy.save()

    其次,让我们使用import numpy as np import random lst = [random.randint(0, 10) for _ in range(100000000)] np.save('lst', lst, allow_pickle=False) 模块而不是IPython的魔术命令timeit。这样做的原因来自在Python 3.5中执行的以下测试:

    %timeit

    将此与同一版本的Python中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 的结果进行比较:

    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 包相比,IPython的魔法%timeit增加了一些时间。因此,我将在我的测试中专门使用timeit

    注意:在下面的讨论中,我将仅使用最小时间(timeit)。

    Python 3.5.3中的测试:

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

    min(t)

    注意第二次测试(使用>>> 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 < if = (5).__lt__不相同(或几乎相同){ {1}}从代码的角度来看。

    第2组:&#34;个人&#34;功能测试

    5 < i

    注意第一次测试(>>> 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 < 1f = (5).__lt__不相同(或几乎相同)从代码的角度来看。

    Python 3.6.2中的测试:

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

    5 < i

    第2组:&#34;个人&#34;功能测试

    >>> 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
    

    注意第一次测试(>>> 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 < 1f = (5).__lt__不相同(或几乎相同)从代码的角度来看。

    讨论

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

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

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

答案 1 :(得分:0)

我认为公平比较会涉及使用相同的功能。在您的示例中,当比较公平时,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

但是,当使用相同的函数时,{3.5}和3.6中的map比列表理解快2倍。

编辑(对@ user2357112评论的回复):

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

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

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

更快

因此,我建议引入f = (5).__lt__并在map和列表理解测试中使用相同的函数。我还通过增加列表中的元素数量并重新使用&#34;循环次数来修改@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)相对性能{{1}对比&#34;列表理解&#34;在Python 3.6中是map,而在Python 3.5中它是67.1/25.8 = 2.60。因此,有趣的是知道为什么@MSeifert测试表明{3.6}中的56.4/38.3 = 1.47较慢。我上面的上一次测试显示map&#34;单独&#34;的时间测试。我不确定这个测试是多么有效(不幸的是) - 我想避免使用f(1)map来消除一个变量 - 但它表明在Python 3.6中[for]变得慢于Python 3.5。因此,我得出结论,它是函数f = (5).__lt__f)的特定形式,其评估速度减慢而不是(5).__lt__函数。我最后才知道这一点#34;测试可能是一个糟糕的测试,但mapmap一起使用时速度非常快(相对或绝对)这一事实表明问题出现在abs而不是{{1} }}

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