Чи розуміння списку та функціональні функції швидші, ніж "для циклів"?


155

З точки зору продуктивності в Python, список осягнення, або функції , такі як map(), filter()і reduce()швидше , ніж цикл? Чому, технічно, вони працюють із швидкістю С , тоді як цикл for працює на швидкості віртуальної машини python ?.

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

Відповіді:


146

Далі наведено грубі вказівки та освічені здогадки на основі досвіду. Ти повинен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% прискорення з тими ж зусиллями), щоб кусати кулю і написати трохи С.


25

Якщо ви перевірите інформацію на 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.


17
Хоча ця сторінка добре прочитана і частково пов’язана, цитування цих номерів не є корисним, можливо, навіть оманливим.

1
Це не вказує на те, що ви терміни. Відносна ефективність сильно відрізнятиметься в залежності від того, що знаходиться в циклі / listcomp / map.
user2357112 підтримує Моніку

@delnan Я згоден Я змінив свою відповідь на заклик ОП прочитати документацію, щоб зрозуміти різницю в продуктивності.
Ентоні Конг

@ user2357112 Ви повинні прочитати сторінку вікі, яку я пов’язав для контексту. Я розмістив його для довідки ОП.
Ентоні Конг

13

Ви запитуєте конкретно про 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')

2
Виглядає як доволі суперечливий спосіб відповісти на це питання. Чи можете ви це зробити, щоб мати кращий сенс?
Аарон Холл

2
@AaronHall Я фактично вважаю відповідь andreipmbcn досить цікавою, оскільки це нетривіальний приклад. Код, з яким ми можемо грати.
Ентоні Конг

@AaronHall, ти хочеш, щоб я відредагував абзац тексту, щоб він звучав чіткіше і простіше, чи ти хочеш, щоб я редагував код?
andreipmbcn

9

Я написав простий сценарій, який перевіряв швидкість, і ось що я дізнався. Власне, цикл був найшвидшим у моєму випадку. Що мене справді здивувало, перевірте нижче (було підрахунок суми квадратів).

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

З python 3.6.1 різниці не такі великі; Скорочення та карта знизяться до 0,24, а розуміння списку - до 0,29. Бо вище, на 0,18.
jjmerelo

Усунення intв square_sum4також робить це трохи швидше і трохи повільніше, ніж цикл for.
jjmerelo

6

Я змінив @ 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 використовує додавання, яке, звичайно, певною мірою повільне
  • розуміння списку не тільки витрачало найменший час на створення списку, але й робить sumнабагато швидшим, на відміну відmap

5

Додаючи поворот до відповіді Алфії , насправді цикл 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.40GHz

0:00:00.257703 #Reduce
0:00:00.184898 #For loop
0:00:00.031718 #Map
0:00:00.212699 #List comprehension

2
square_sum3 та square_sum4 невірні. Вони не дадуть суму. Відповідь нижче від @alisca chen насправді правильна.
ShikharDua

3

Мені вдалося змінити частину коду @ alpiii і виявив, що розуміння списку трохи швидше, ніж для циклу. Це може бути викликано int(), це не справедливо між розумінням списку та циклом.

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 #Reduce

0:00:00.089216 #For loop

0:00:00.101532 #Map

0:00:00.068916 #List comprehension
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.