如果有一个函数/方法是用C编写的,为了获得一点额外的性能,我有时会使用map。然而,最近我重新审视了一些基准测试,发现在Python3.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)

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

最佳答案

我认为一个公平的比较包括在Python3.5和3.6中使用相同的函数和相同的测试条件,以及在所选的Python版本中比较map以列出理解。
在我最初的回答中,我进行了多次测试,结果表明,在两个版本的python中,与列表理解相比,map仍然快了大约两倍。然而,有些结果并不是决定性的,所以我做了更多的测试。
首先,让我引用你在问题中陈述的一些观点:
“……”[i]注意到,python 3.5和3.6之间的相对性能(与类似的列表理解相比)发生了巨大的变化。
你也会问:
“我的问题是,在这种情况下,是什么使得列表理解更快,映射解决方案也更慢?”
如果您的意思是map比python 3.6中的list理解慢,或者您的意思是python 3.6中的map比3.5中的慢,并且list理解的性能有所提高(尽管不一定达到beatmap的级别),则不太清楚。
根据我第一次回答这个问题后进行的更广泛的测试,我认为我对正在发生的事情有了一个概念。
然而,首先让我们为“公平”比较创造条件。为此,我们需要:
比较不同python版本中使用相同函数的map性能;
比较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.


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.

让我们首先解决“相同数据”的问题。不幸的是,由于您有效地使用了map,所以两个版本的python上的每个数据集都是不同的。这可能导致了在两个Python版本上看到的性能差异。一个解决方法是设置,例如,seed(None)(或类似的方法)。我选择创建一次列表并使用lst保存它,然后在每个版本中加载它。这一点尤其重要,因为我选择了稍微修改您的测试(循环数和重复数),并且我将您的数据集的长度增加到了100000000:
import numpy as np
import random
lst = [random.randint(0, 10) for _ in range(100000000)]
np.save('lst', lst, allow_pickle=False)

其次,让我们使用random.seed(0)模块而不是ipython的magic命令。这样做的原因来自于在Python3.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中的numpy.save()结果进行比较:
>>> 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的magic%timeit增加了一些时间。因此,我将只在测试中使用timeit
注意:在接下来的讨论中,我将只使用最小计时(%timeit)。
python 3.5.3中的测试:
第一组:地图和列表理解测试
>>> 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

注意第二个测试(使用timeit列出理解)比第三个测试(使用timeit列出理解)慢得多,这表明从代码的角度来看min(t)f(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

请注意,第一个测试(属于5 < i的测试)比第二个测试(属于f = (5).__lt__的测试)慢得多,进一步支持从代码的角度来看5 < if(1)不相同(或几乎相同)。
python 3.6.2中的测试:
第一组:地图和列表理解测试
>>> 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

请注意,第一个测试(属于5 < 1的测试)比第二个测试(属于f = (5).__lt__的测试)慢得多,进一步支持从代码的角度来看5 < if(1)不相同(或几乎相同)。
讨论
我不知道这些定时测试有多可靠,而且很难分离出导致这些定时结果的所有因素。但是,我们可以从测试的“组2”中注意到,唯一显著改变其计时的“单个”测试是5 < 1:它在python 3.6中从python 3.5中的0.0309s下降到了0.0268s。这使得python 3.6中的列表理解测试比python3.5中的类似测试运行得更快。然而,这并不意味着清单理解在Python3.6中会变得更快。
让我们比较一下f = (5).__lt__的相对性能,以列出在同一个Python版本中对同一个函数的理解。然后我们进入python 3.5:5 < i5 < 1和python 3.6:5 < imap。基于这些相对性能,我们可以看到,在python 3.6中,r(f) = 7.4428/4.6666 = 1.595相对于列表理解性能的性能至少与python 3.5对于r(abs) = 5.665/4.167 = 1.359函数的性能相同,而且这个比率对于python 3.6中的r(f) = 7.316/4.5997 = 1.591函数甚至有所提高。
在任何情况下,我相信没有证据表明在python 3.6中,无论是相对的还是绝对的,列表理解都变得更快。唯一的性能改进是针对r(abs) = 6.218/2.743 = 2.267测试的,但这是因为在Python3.6中map本身变得更快,而不是因为列表理解本身更快。

关于python - Python 3.5 vs. 3.6使得“map”与理解相比变慢的原因,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/45601663/

10-15 08:48