Вопрос-Ответ

Are list-comprehensions and functional functions faster than "for loops"?

Понимание списков и функциональных функций быстрее, чем "for loops"?

С точки зрения производительности в Python, является ли понимание списков или функций, таких как map(), filter() и reduce(), быстрее, чем цикл for? Почему технически они выполняются со скоростью C, в то время как цикл for выполняется со скоростью виртуальной машины python?.

Предположим, что в игре, которую я разрабатываю, мне нужно рисовать сложные и огромные карты, используя for loops. Этот вопрос был бы определенно актуален, поскольку, если понимание списков, например, действительно быстрее, это был бы намного лучший вариант, чтобы избежать задержек (несмотря на визуальную сложность кода).

Переведено автоматически
Ответ 1

Ниже приведены приблизительные рекомендации и обоснованные предположения, основанные на опыте. Вам следует timeit или профилировать свой конкретный вариант использования, чтобы получить точные цифры, и эти цифры иногда могут не совпадать с приведенными ниже.

Понимание списка обычно происходит немного быстрее, чем точно эквивалентный for цикл (который фактически создает список), скорее всего, потому, что ему не нужно искать список и его append метод на каждой итерации. Однако понимание списков по-прежнему выполняет цикл на уровне байт-кода:

>>> dis.dis(<the code object for `[x for x in range(10)]`>)
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (x)
12 LOAD_FAST 1 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE

Использование понимания списка вместо цикла, который не создает список, бессмысленно накапливая список бессмысленных значений, а затем выбрасывая список, часто медленнее из-за накладных расходов на создание и расширение списка. Понимание списков - это не волшебство, которое по своей сути быстрее, чем старый добрый цикл.

Что касается функциональных функций обработки списков: хотя они написаны на C и, вероятно, превосходят эквивалентные функции, написанные на Python, они не обязательно являются самым быстрым вариантом. Ожидается некоторое ускорение, если функция тоже написана на C. Но в большинстве случаев при использовании lambda (или другой функции Python) накладные расходы на многократную настройку фреймов стека Python и т.д. Съедают любую экономию. Простое выполнение той же работы в строке, без вызовов функций (например, понимание списка вместо map или filter) часто немного быстрее.


Предположим, что в игре, которую я разрабатываю, мне нужно рисовать сложные и огромные карты, используя for loops. Этот вопрос был бы определенно актуален, поскольку, если понимание списков, например, действительно быстрее, это был бы намного лучший вариант, чтобы избежать задержек (несмотря на визуальную сложность кода).


Скорее всего, если подобный код уже недостаточно быстр при написании на хорошем не "оптимизированном" Python, никакая микрооптимизация на уровне Python не сделает его достаточно быстрым, и вам следует начать думать о переходе на C. Хотя обширные микрооптимизации часто могут значительно ускорить код Python, существует низкий (в абсолютном выражении) предел для этого. Более того, даже до того, как вы достигнете этого предела, становится просто более экономичным (ускорение на 15% по сравнению с ускорением на 300% при тех же усилиях) стиснуть зубы и написать немного C.

Ответ 2

Если вы проверите информацию на странице python.org , вы можете увидеть это краткое описание:

Version Time (seconds)
Basic loop 3.47
Eliminate dots 2.45
Local variable & no dots 1.79
Using map function 0.54

Но вы действительно должны подробно прочитать приведенную выше статью, чтобы понять причину разницы в производительности.

Я также настоятельно рекомендую вам засекать время в вашем коде с помощью timeit. В конце концов, может возникнуть ситуация, когда, например, вам может потребоваться выйти из for цикла при выполнении условия. Потенциально это может быть быстрее, чем узнать результат с помощью вызова map.

Ответ 3

Я модифицировал код @ Alisa и использовал cProfile, чтобы показать, почему понимание списков происходит быстрее:

from functools import reduce
import datetime

def reduce_(numbers):
return reduce(lambda sum, next: sum + next * next, numbers, 0)

def for_loop(numbers):
a = []
for i in numbers:
a.append(i*2)
a = sum(a)
return a

def map_(numbers):
sqrt = lambda x: x*x
return sum(map(sqrt, numbers))

def list_comp(numbers):
return(sum([i*i for i in numbers]))

funcs = [
reduce_,
for_loop,
map_,
list_comp
]

if __name__ == "__main__":
# [1, 2, 5, 3, 1, 2, 5, 3]
import cProfile
for f in funcs:
print('=' * 25)
print("Profiling:", f.__name__)
print('=' * 25)
pr = cProfile.Profile()
for i in range(10**6):
pr.runcall(f, [1, 2, 5, 3, 1, 2, 5, 3])
pr.create_stats()
pr.print_stats()

Вот результаты:

=========================
Profiling: reduce_
=========================
11000000 function calls in 1.501 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1000000 0.162 0.000 1.473 0.000 profiling.py:4(reduce_)
8000000 0.461 0.000 0.461 0.000 profiling.py:5(<lambda>)
1000000 0.850 0.000 1.311 0.000 {built-in method _functools.reduce}
1000000 0.028 0.000 0.028 0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: for_loop
=========================
11000000 function calls in 1.372 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1000000 0.879 0.000 1.344 0.000 profiling.py:7(for_loop)
1000000 0.145 0.000 0.145 0.000 {built-in method builtins.sum}
8000000 0.320 0.000 0.320 0.000 {method 'append' of 'list' objects}
1000000 0.027 0.000 0.027 0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: map_
=========================
11000000 function calls in 1.470 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1000000 0.264 0.000 1.442 0.000 profiling.py:14(map_)
8000000 0.387 0.000 0.387 0.000 profiling.py:15(<lambda>)
1000000 0.791 0.000 1.178 0.000 {built-in method builtins.sum}
1000000 0.028 0.000 0.028 0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: list_comp
=========================
4000000 function calls in 0.737 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1000000 0.318 0.000 0.709 0.000 profiling.py:18(list_comp)
1000000 0.261 0.000 0.261 0.000 profiling.py:19(<listcomp>)
1000000 0.131 0.000 0.131 0.000 {built-in method builtins.sum}
1000000 0.027 0.000 0.027 0.000 {method 'disable' of '_lsprof.Profiler' objects}

ИМХО:


  • reduce и map в целом довольно медленные. Мало того, использование sum возвращаемых map итераторов происходит медленно по сравнению с sumредактированием списка

  • for_loop использует append, который, конечно, в некоторой степени медленный

  • понимание списков не только затрачивает меньше всего времени на построение списка, но и делает sum намного быстрее, в отличие от map

Ответ 4

Вы спрашиваете конкретно о map(), filter() и reduce(), но я предполагаю, что вы хотите знать о функциональном программировании в целом. Я сам протестировал это на задаче вычисления расстояний между всеми точками внутри набора точек, функциональное программирование (с использованием starmap функции из встроенного itertools модуля) оказалось немного медленнее, чем for-loops (фактически, оно занимает в 1,25 раза больше времени). Вот пример кода, который я использовал:

import itertools, time, math, random

class Point:
def __init__(self,x,y):
self.x, self.y = x, y

point_set = (Point(0, 0), Point(0, 1), Point(0, 2), Point(0, 3))
n_points = 100
pick_val = lambda : 10 * random.random() - 5
large_set = [Point(pick_val(), pick_val()) for _ in range(n_points)]
# the distance function
f_dist = lambda x0, x1, y0, y1: math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2)
# go through each point, get its distance from all remaining points
f_pos = lambda p1, p2: (p1.x, p2.x, p1.y, p2.y)

extract_dists = lambda x: itertools.starmap(f_dist,
itertools.starmap(f_pos,
itertools.combinations(x, 2)))

print('Distances:', list(extract_dists(point_set)))

t0_f = time.time()
list(extract_dists(large_set))
dt_f = time.time() - t0_f

Функциональная версия быстрее процедурной?

def extract_dists_procedural(pts):
n_pts = len(pts)
l = []
for k_p1 in range(n_pts - 1):
for k_p2 in range(k_p1, n_pts):
l.append((pts[k_p1].x - pts[k_p2].x) ** 2 +
(pts[k_p1].y - pts[k_p2].y) ** 2)
return l

t0_p = time.time()
list(extract_dists_procedural(large_set))
# using list() on the assumption that
# it eats up as much time as in the functional version

dt_p = time.time() - t0_p

f_vs_p = dt_p / dt_f
if f_vs_p >= 1.0:
print('Time benefit of functional progamming:', f_vs_p,
'times as fast for', n_points, 'points')
else:
print('Time penalty of functional programming:', 1 / f_vs_p,
'times as slow for', n_points, 'points')
python performance