La comprensione dell'elenco e le funzioni funzionali sono più veloci di "for loop"?


155

In termini di prestazioni in Python, una comprensione dell'elenco, o funzioni simili map(), filter()e reduce()più veloce di un ciclo for? Perché, tecnicamente, funzionano a una velocità C , mentre il ciclo for viene eseguito alla velocità della macchina virtuale Python ?

Supponiamo che in un gioco che sto sviluppando ho bisogno di disegnare mappe complesse ed enormi usando per i loop. Questa domanda sarebbe sicuramente pertinente, poiché se una comprensione dell'elenco, ad esempio, è effettivamente più veloce, sarebbe un'opzione molto migliore per evitare ritardi (nonostante la complessità visiva del codice).

Risposte:


146

Di seguito sono riportate linee guida approssimative e ipotesi ponderate basate sull'esperienza. Dovresti timeito profila il tuo caso d'uso concreto per ottenere numeri concreti, e questi numeri possono occasionalmente non essere d'accordo con quanto segue.

Una comprensione dell'elenco è di solito un po 'più veloce del forloop esattamente equivalente (che in realtà crea un elenco), molto probabilmente perché non deve cercare l'elenco e il suo appendmetodo su ogni iterazione. Tuttavia, una comprensione dell'elenco esegue ancora un ciclo a livello di bytecode:

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

L'uso di una comprensione dell'elenco al posto di un ciclo che non crea un elenco, accumula insensatamente un elenco di valori insignificanti e quindi butta via l'elenco, è spesso più lento a causa del sovraccarico di creare ed estendere l'elenco. La comprensione dell'elenco non è una magia intrinsecamente più veloce di un buon vecchio ciclo.

Per quanto riguarda le funzioni funzionali lista di elaborazione: Mentre questi sono scritti in C e probabilmente sovraperformare funzioni equivalenti scritti in Python, sono non necessariamente l'opzione più veloce. È prevista una certa accelerazione se la funzione è scritta anche in C. Ma la maggior parte dei casi che utilizza una lambda(o altra funzione Python), il sovraccarico di impostare ripetutamente frame di stack Python ecc. Fa risparmiare tutti i risparmi. Fare semplicemente lo stesso lavoro in linea, senza chiamate di funzione (ad esempio una comprensione dell'elenco anziché mapo filter) è spesso leggermente più veloce.

Supponiamo che in un gioco che sto sviluppando ho bisogno di disegnare mappe complesse ed enormi usando per i loop. Questa domanda sarebbe sicuramente pertinente, poiché se una comprensione dell'elenco, ad esempio, è effettivamente più veloce, sarebbe un'opzione molto migliore per evitare ritardi (nonostante la complessità visiva del codice).

È probabile che se un codice del genere non è già abbastanza veloce quando è scritto in un buon Python non "ottimizzato", nessuna quantità di micro ottimizzazione a livello di Python lo renderà abbastanza veloce e dovresti iniziare a pensare di passare a C. le micro ottimizzazioni possono spesso velocizzare notevolmente il codice Python, c'è un limite basso (in termini assoluti) a questo. Inoltre, anche prima di raggiungere quel limite, diventa semplicemente più efficiente in termini di costi (15% di velocità contro il 300% di velocità con lo stesso sforzo) mordere il proiettile e scrivere un po 'di C.


25

Se controlli le informazioni su python.org , puoi vedere questo riepilogo:

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

Ma è davvero necessario leggere l'articolo di cui sopra in dettaglio per capire la causa della differenza di prestazioni.

Consiglio anche vivamente che dovresti cronometrare il tuo codice usando timeit . Alla fine della giornata, potrebbe esserci una situazione in cui, ad esempio, potrebbe essere necessario uscire dal forcircuito quando si verifica una condizione. Potrebbe potenzialmente essere più veloce di scoprire il risultato chiamando map.


17
Mentre quella pagina è una buona lettura e in parte correlata, solo citare quei numeri non è utile, forse anche fuorviante.

1
Questo non dà indicazioni su cosa stai facendo. Le prestazioni relative variano notevolmente a seconda di ciò che è nel loop / listcomp / map.
user2357112 supporta Monica

@delnan Sono d'accordo. Ho modificato la mia risposta per sollecitare OP a leggere la documentazione per comprendere la differenza nelle prestazioni.
Anthony Kong

@ user2357112 Devi leggere la pagina wiki che ho collegato per il contesto. L'ho pubblicato come riferimento per OP.
Anthony Kong,

13

Tu chiedi specificamente di map(), filter()e reduce(), ma suppongo che tu voglia conoscere la programmazione funzionale in generale. Avendo testato questo me stesso sul problema del calcolo delle distanze tra tutti i punti all'interno di una serie di punti, la programmazione funzionale (utilizzando la starmapfunzione dal itertoolsmodulo integrato) si è rivelata leggermente più lenta rispetto ai for-loop (impiegando 1,25 volte di più, in fatto). Ecco il codice di esempio che ho usato:

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

La versione funzionale è più veloce della versione procedurale?

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
Sembra un modo piuttosto contorto per rispondere a questa domanda. Puoi abbatterlo in modo che abbia un senso migliore?
Aaron Hall

2
@AaronHall In realtà trovo la risposta di andreipmbcn piuttosto interessante perché è un esempio non banale. Codice con cui possiamo giocare.
Anthony Kong,

@AaronHall, vuoi che modifichi il paragrafo di testo in modo che suoni più chiaro e diretto, o vuoi che modifichi il codice?
Andreipmbcn

9

Ho scritto una semplice sceneggiatura che prova la velocità e questo è quello che ho scoperto. In realtà per il loop è stato il più veloce nel mio caso. Mi ha davvero sorpreso, guarda qui sotto (stava calcolando la somma dei quadrati).

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

Con Python 3.6.1 le differenze non sono così grandi; Riduci e mappa scendi a 0,24 e elenca la comprensione a 0,29. Perché è più alto, a 0,18.
jjmerelo,

L'eliminazione di intin square_sum4lo rende anche un po 'più veloce e solo un po' più lento del ciclo for.
jjmerelo,

6

Ho modificato il codice di @Alisa e usato cProfileper mostrare perché la comprensione dell'elenco è più veloce:

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()

Ecco i risultati:

=========================
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}

A PARER MIO:

  • reducee mapin generale sono piuttosto lenti. Non solo, l'utilizzo sumsugli iteratori maprestituiti è lento, rispetto a sumun elenco
  • for_loop usa append, che ovviamente è lento in una certa misura
  • la comprensione della lista non solo ha trascorso il minor tempo a costruire la lista, ma rende anche summolto più veloce, al contrario dimap

5

Aggiungendo una svolta alla risposta di Alphii , in realtà il ciclo for sarebbe il secondo migliore e circa 6 volte più lento dimap

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])

Le principali modifiche sono state quelle di eliminare le sumchiamate lente , nonché quelle probabilmente non necessarie int()nell'ultimo caso. Mettere il ciclo for e la mappa negli stessi termini lo rende piuttosto un dato di fatto, in realtà. Ricorda che i lambda sono concetti funzionali e teoricamente non dovrebbero avere effetti collaterali, ma, beh, possono avere effetti collaterali come l'aggiunta a a. Risultati in questo caso con 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 e square_sum4 non sono corretti. Non daranno la somma. La risposta di seguito da @alisca chen è in realtà corretta.
ShikharDua,

3

Sono riuscito a modificare parte del codice di @ alpiii e ho scoperto che la comprensione dell'elenco è un po 'più veloce rispetto a quella di loop. Potrebbe essere causato da int(), non è giusto tra la comprensione dell'elenco e il ciclo.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.