Являются ли списки-функции и функциональные функции быстрее, чем "для циклов"?

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

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

Ответ 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) часто выполняется немного быстрее.

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

Скорее всего, если такой код не будет достаточно быстрым, если он написан на хорошем не оптимизированном 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

Вы спрашиваете конкретно о map(), filter() и reduce(), но я предполагаю, что вы хотите знать о функциональном программировании в целом. Испытав это самостоятельно на проблеме вычисления расстояний между всеми точками в пределах набора точек, функциональное программирование (с использованием функции starmap из встроенного модуля itertools) оказалось немного медленнее, чем для циклов (в 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')

Ответ 4

Я написал простой script, который проверяет скорость, и это то, что я узнал. На самом деле цикл был самым быстрым в моем случае. Это меня действительно удивило, посмотри ниже (вычислял сумму квадратов).

from functools import reduce
import datetime


def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

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


def square_sum2(numbers):
    a = 0
    for i in numbers:
        i = i**2
        a += i
    return a

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

def square_sum4(numbers):
    return(sum([int(i)**2 for i in numbers]))


time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])

0:00:00.302000 #Reduce 0:00:00.144000 #For loop 0:00:00.318000 #Map 0:00:00.390000 #List comprehension

Ответ 5

Добавляя завихрение к ответу Alphii, фактически цикл for будет вторым лучшим и примерно в 6 раз медленнее, чем map

from functools import reduce
import datetime


def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

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


def square_sum2(numbers):
    a = 0
    for i in numbers:
        a += i**2
    return a

def square_sum3(numbers):
    a = 0
    map(lambda x: a+x**2, numbers)
    return a

def square_sum4(numbers):
    a = 0
    return [a+i**2 for i in numbers]

time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])

Основные изменения заключались в том, чтобы устранить вызовы с медленной sum, а также, вероятно, ненужный int() в последнем случае. На самом деле включение цикла for и карты в одни и те же выражения делает его довольно фактом. Помните, что лямбды являются функциональными концепциями и теоретически не должны иметь побочных эффектов, но, может быть, они могут иметь побочные эффекты, такие как добавление к a. Результаты в этом случае с Python 3.6.1, Ubuntu 14.04, Intel (R) Core (TM) i7-4770 CPU @3,40 ГГц

0:00:00.257703
0:00:00.184898
0:00:00.031718
0:00:00.212699

Ответ 6

Мне удалось изменить часть кода @alpiii и обнаружил, что понимание списка немного быстрее, чем для цикла. Это может быть вызвано int(), это не справедливо между пониманием списка и циклом for.

from functools import reduce
import datetime
def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)
def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next*next, numbers, 0)
def square_sum2(numbers):
    a = []
    for i in numbers:
        a.append(i*2)
    a = sum(a)
    return a
def square_sum3(numbers):
    sqrt = lambda x: x*x
    return sum(map(sqrt, numbers))
def square_sum4(numbers):
    return(sum([i*i for i in numbers]))
time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])

0: 00: 00,101122

0: 00: 00,089216

0: 00: 00,101532

0: 00: 00,068916