Python 3.5 против 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]

Я понимаю, что использовать (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 медленнее? Я понимаю, что разница не так уж велика, мне просто стало любопытно, потому что это был один из «трюков», который я иногда (на самом деле редко) использовал в кодах, критически важных для производительности.


person MSeifert    schedule 09.08.2017    source источник
comment
Хм, может быть, это зависит от функции? например, 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.2017
comment
Этот шаблон справедлив для int и float ... и это в значительной степени исчерпывает мои варианты использования list(map(...)) по сравнению со списками;)   -  person juanpa.arrivillaga    schedule 10.08.2017
comment
@juanpa.arrivillaga Я могу подтвердить ваши наблюдения. Однако я также вижу снижение производительности с помощью map и увеличение производительности для понимания других методов, например. 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.2017
comment
@poke Я подумал, что это самое честное сравнение, потому что байт-код COMPARE_OP — это самый быстрый способ из кода Python вызвать метод __lt__, который также вызывает map (в данном случае). Оба способа фактически избегают (большинства) накладных расходов на вызов функции таким образом.   -  person MSeifert    schedule 10.08.2017
comment
Возможные кандидаты на замедление map включают изменение map.__next__, которое не окупается, когда сопоставленная функция (5).__lt__. (версия 3.5, версия 3.6) Разница достаточно мала, чтобы найти истинную причину и проверить, что это причина, может быть сложно.   -  person user2357112 supports Monica    schedule 10.08.2017
comment
Я добавил к своему ответу дополнительные тесты, которые, как мне кажется, показывают, что оценка f = (5).__lt__ выполняется медленнее в Python 3.6 по сравнению с Python 3.5.   -  person AGN Gazer    schedule 10.08.2017
comment
@user2357112 user2357112 Из этого теста нельзя сделать вывод, что «карта стала медленнее». Вы можете сказать, что «этот конкретный тест стал медленнее по сравнению с этим конкретным пониманием списка», что может быть вызвано большим количеством причин, хотя одна из них заключается в том, что map стал медленнее, но столь же вероятно, что понимание списка в целом стало быстрее. Поскольку оба делают очень разные вещи, вы не можете приписать разницу только map. — На самом деле мои собственные тесты показывают, что в целом время выполнения стало быстрее (по крайней мере, для меня), а понимание списка OP стало намного быстрее по сравнению с другими пониманиями.   -  person poke    schedule 10.08.2017
comment
@poke: я не утверждаю, что сам map, должно быть, стал медленнее. Язык неоднозначен; моя ссылка на замедление map не предназначалась для того, чтобы утверждать, что виновником является само map, точно так же, как если бы я говорил о замедлении своего компьютера, я бы не утверждал, что мое физическое оборудование должно быть виновато. (Фрагмент, использующий map, действительно работал медленнее в абсолютном выражении в таймингах MSeifert Python 3.6, поэтому объяснение только с точки зрения улучшения понимания списка не будет удовлетворительным.)   -  person user2357112 supports Monica    schedule 11.08.2017


Ответы (2)


Я думаю, что честное сравнение предполагает использование одной и той же функции и одинаковых условий тестирования в 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.

и

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
comment
Вы подняли хорошие оценки для общего случая (большой признательности!). Однако я до сих пор не знаю, в чем причина того, что map ~20% медленнее, а понимание ~20% быстрее в моем случае. Обратите внимание, что сравнение было преднамеренно несправедливым (за исключением проблемы заполнения — я пропустил это — но оно дает одинаковые результаты, если я использую pickle для использования одного и того же списка в обеих версиях). Он сравнил самый быстрый способ решения проблемы с map с самым быстрым способом решения проблемы с пониманием списка. :) - person MSeifert; 10.08.2017
comment
@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 практически не изменилась (в пределах ошибок) и список компр. производительность увеличилась очень скромно и почти достигла той же скорости, что и map (с учетом ошибок). Я, конечно, не вижу 20-процентного снижения производительности map при расширении тестов (использование timeit, идентичные массивы данных, увеличение размера данных и т. д.), как описано выше. - person AGN Gazer; 11.08.2017
comment
@ user8371915 Спасибо! - person AGN Gazer; 30.07.2018

Я думаю, что честное сравнение будет включать использование одной и той же функции. В случае вашего примера, когда сравнение справедливо, 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 примерно в 2 раза быстрее, чем понимание списка как в Python 3.5, так и в 3.6.

РЕДАКТИРОВАТЬ (ответ на комментарии @user2357112):

Я считаю, что выполнение «добросовестных» сравнений важно для ответа на вопрос ОП: «Мой вопрос: что произошло в этом случае, что ускорило понимание списка и замедлило решение карты?» (последний абзац ). Однако в первом абзаце @MSeifert говорит: "... [Я] заметил, что относительная производительность (по сравнению с аналогичным пониманием списка) резко изменилась между Python 3.5 и 3.6"< /em> То есть сравнение идет между map и list 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, так и в тестах на понимание списка. Я также изменил тест @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, а не в map.

ПРИМЕЧАНИЕ. Python 3.5 использует IPython 5.3.0, а Python 3.6 использует IPython 6.1.0.

person AGN Gazer    schedule 09.08.2017
comment
Вопрос в том, почему сравнение производительности изменилось между 3,5 и 3,6. Справедливо ли сравнение, не имеет значения. - person user2357112 supports Monica; 10.08.2017
comment
@user2357112 Нет. @MSeifert говорит: ... [Я] заметил, что относительная производительность (по сравнению с аналогичным пониманием списка) резко изменилась между Python 3.5 и 3.6 Мой эксперимент показывает, что при использовании идентичных функций (=добросовестное сравнение) относительная производительность между map и пониманием списков не изменилась по сравнению с Python 3.5 и 3.6 (по крайней мере не решительно), что изменилось в ОП. - person AGN Gazer; 10.08.2017
comment
Хотя я согласен с тем, что разница невелика (и не согласен с вашим суждением о справедливости), это не дает ответа на вопрос. - person user2357112 supports Monica; 10.08.2017
comment
@user2357112 user2357112 Я добавил дополнительные тесты, которые, как мне кажется, показывают, что оценка f = (5).__lt__ в Python 3.6 выполняется медленнее, чем в Python 3.5. - person AGN Gazer; 10.08.2017