Я думаю, что честное сравнение предполагает использование одной и той же функции и одинаковых условий тестирования в Python 3.5 и 3.6, а также при сравнении map
с пониманием списка в выбранной версии Python.
В своем первоначальном ответе я провел несколько тестов, которые показали, что map
по-прежнему работает примерно в два раза быстрее в обеих версиях Python по сравнению с пониманием списка. Однако некоторые результаты не были окончательными, поэтому я провел еще несколько тестов.
Сначала позвольте мне привести некоторые из ваших пунктов, изложенных в вопросе:
"... [Я] заметил, что относительная производительность [map
] (по сравнению с аналогичным распознаванием списков) резко изменилась между Python 3.5 и 3.6"
Вы также спросите:
"Мой вопрос: что произошло в этом случае, из-за чего понимание списка ускорилось, а решение карты замедлилось?"
Не очень ясно, имеете ли вы в виду, что карта медленнее, чем понимание списка в Python 3.6, или вы имеете в виду, что карта в Python 3.6 медленнее, чем в 3.5, и производительность понимания списка увеличилась (хотя и не обязательно до уровня избиения map
).
Основываясь на более обширных тестах, которые я провел после своего первого ответа на этот вопрос, я думаю, что имею представление о том, что происходит.
Однако сначала создадим условия для «честных» сравнений. Для этого нам необходимо:
Сравните производительность map
в разных версиях Python, используя одну и ту же функцию;
Сравните производительность 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.
Давайте сначала обратимся к проблеме «одинаковых данных». К сожалению, поскольку вы фактически используете 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
Сравните это с результатом 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
По неизвестным мне причинам магия IPython %timeit
прибавляет время по сравнению с пакетом 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 < 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
Обратите внимание, как снова первый тест (из f(1)
) значительно медленнее второго теста (из 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
Группа 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 < 1
), что еще раз подтверждает, что f = (5).__lt__
не идентичен (или почти идентичен) 5 < i
с точки зрения кода.
Обсуждение
Я не знаю, насколько надежны эти временные тесты, и также трудно выделить все факторы, влияющие на эти временные результаты. Однако из «Группы 2» тестов мы можем заметить, что единственным «индивидуальным» тестом, который значительно изменил свое время, является тест 5 < 1
: он снизился до 0,0268 с в Python 3.6 с 0,0309 с в Python 3.5. Это делает тест понимания списка в Python 3.6, использующий 5 < i
, работать быстрее, чем аналогичный тест в Python 3.5. Однако это не означает, что понимание списков стало быстрее в Python 3.6.
Давайте сравним относительную производительность map
с пониманием списка для той же функции в той же версии Python. Затем мы получаем в 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, а не из-за того, что само понимание списка стало быстрее.
person
AGN Gazer
schedule
10.08.2017
str
по-прежнему работает быстрее сmap
, поэтомуtimeit.timeit("list(map(str, range(100000)))", number=1000)
дал мне18.98376033702516
, аtimeit.timeit("[str(i) for i in range(100000)]", number=1000)
дал мне24.40201253897976
- person juanpa.arrivillaga   schedule 10.08.2017int
иfloat
... и это в значительной степени исчерпывает мои варианты использованияlist(map(...))
по сравнению со списками;) - person juanpa.arrivillaga   schedule 10.08.2017map
и увеличение производительности для понимания других методов, например.a = [' {}'.format(i) for i in range(100000)]
при таймингеlist(map(str.strip, a))
против[i.strip() for i in a]
(хотяmap
там все же быстрее) - person MSeifert   schedule 10.08.2017COMPARE_OP
— это самый быстрый способ из кода Python вызвать метод__lt__
, который также вызываетmap
(в данном случае). Оба способа фактически избегают (большинства) накладных расходов на вызов функции таким образом. - person MSeifert   schedule 10.08.2017map
включают изменениеmap.__next__
, которое не окупается, когда сопоставленная функция(5).__lt__
. (версия 3.5, версия 3.6) Разница достаточно мала, чтобы найти истинную причину и проверить, что это причина, может быть сложно. - person user2357112 supports Monica   schedule 10.08.2017f = (5).__lt__
выполняется медленнее в Python 3.6 по сравнению с Python 3.5. - person AGN Gazer   schedule 10.08.2017map
стал медленнее, но столь же вероятно, что понимание списка в целом стало быстрее. Поскольку оба делают очень разные вещи, вы не можете приписать разницу толькоmap
. — На самом деле мои собственные тесты показывают, что в целом время выполнения стало быстрее (по крайней мере, для меня), а понимание списка OP стало намного быстрее по сравнению с другими пониманиями. - person poke   schedule 10.08.2017map
, должно быть, стал медленнее. Язык неоднозначен; моя ссылка на замедлениеmap
не предназначалась для того, чтобы утверждать, что виновником является самоmap
, точно так же, как если бы я говорил о замедлении своего компьютера, я бы не утверждал, что мое физическое оборудование должно быть виновато. (Фрагмент, использующийmap
, действительно работал медленнее в абсолютном выражении в таймингах MSeifert Python 3.6, поэтому объяснение только с точки зрения улучшения понимания списка не будет удовлетворительным.) - person user2357112 supports Monica   schedule 11.08.2017