Les compréhensions de listes et les fonctions fonctionnelles sont-elles plus rapides que les «boucles for»?

156

En termes de performances en Python, est-ce qu'une liste-compréhension, ou des fonctions similaires map(), est- filter()elle reduce()plus rapide qu'une boucle for? Pourquoi, techniquement, ils fonctionnent à une vitesse C , alors que la boucle for s'exécute à la vitesse de la machine virtuelle python ?.

Supposons que dans un jeu que je développe, j'ai besoin de dessiner des cartes complexes et énormes en utilisant des boucles for. Cette question serait certainement pertinente, car si une liste-compréhension, par exemple, est effectivement plus rapide, ce serait une bien meilleure option afin d'éviter les décalages (malgré la complexité visuelle du code).

Ericson Willians
la source

Réponses:

147

Ce qui suit sont des directives approximatives et des suppositions éclairées basées sur l'expérience. Vous devriez timeitou profiler votre cas d'utilisation concret pour obtenir des chiffres précis, et ces chiffres peuvent parfois être en désaccord avec ce qui suit.

Une compréhension de liste est généralement un tout petit peu plus rapide que la forboucle exactement équivalente (qui construit en fait une liste), probablement parce qu'elle n'a pas à rechercher la liste et sa appendméthode à chaque itération. Cependant, une compréhension de liste effectue toujours une boucle au niveau du 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

Utiliser une compréhension de liste à la place d'une boucle qui ne construit pas de liste, accumuler de manière absurde une liste de valeurs sans signification et ensuite jeter la liste, est souvent plus lent raison de la surcharge de création et d'extension de la liste. Les compréhensions de listes ne sont pas de la magie qui est intrinsèquement plus rapide qu'une bonne vieille boucle.

Quant aux fonctions de traitement de listes fonctionnelles: bien qu'elles soient écrites en C et surpassent probablement les fonctions équivalentes écrites en Python, elles ne sont pas nécessairement l'option la plus rapide. Une certaine accélération est attendue si la fonction est également écrite en C. Mais dans la plupart des cas utilisant une lambda(ou une autre fonction Python), la surcharge de la configuration répétée des cadres de pile Python, etc. consomme des économies. Faire simplement le même travail en ligne, sans appel de fonction (par exemple, une compréhension de liste au lieu de mapou filter) est souvent légèrement plus rapide.

Supposons que dans un jeu que je développe, j'ai besoin de dessiner des cartes complexes et énormes en utilisant des boucles for. Cette question serait certainement pertinente, car si une liste-compréhension, par exemple, est effectivement plus rapide, ce serait une bien meilleure option afin d'éviter les décalages (malgré la complexité visuelle du code).

Il y a de fortes chances que si un code comme celui-ci n'est pas déjà assez rapide lorsqu'il est écrit en bon Python non "optimisé", aucune micro-optimisation de niveau Python ne le rendra assez rapide et vous devriez commencer à penser à passer en C. Les micro-optimisations peuvent souvent accélérer considérablement le code Python, il y a une faible limite (en termes absolus) à cela. De plus, même avant d'atteindre ce plafond, il devient tout simplement plus rentable (15% d'accélération contre 300% d'accélération avec le même effort) de mordre la balle et d'écrire du C.


la source
25

Si vous vérifiez les informations sur python.org , vous pouvez voir ce résumé:

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

Mais vous devriez vraiment lire l'article ci-dessus en détail pour comprendre la cause de la différence de performances.

Je vous suggère également fortement de chronométrer votre code en utilisant timeit . À la fin de la journée, il peut y avoir une situation où, par exemple, vous devrez peut-être sortir de la forboucle lorsqu'une condition est remplie. Cela pourrait potentiellement être plus rapide que de trouver le résultat en appelant map.

Anthony Kong
la source
17
Bien que cette page soit une bonne lecture et en partie liée, le simple fait de citer ces chiffres n'est pas utile, voire trompeur.
1
Cela ne donne aucune indication de ce que vous chronométrez. Les performances relatives varient considérablement en fonction de ce qu'il y a dans la boucle / listcomp / map.
user2357112 prend en charge Monica
@delnan Je suis d'accord. J'ai modifié ma réponse pour inciter OP à lire la documentation pour comprendre la différence de performances.
Anthony Kong
@ user2357112 Vous devez lire la page wiki que j'ai liée pour le contexte. Je l'ai posté pour référence OP.
Anthony Kong
13

Vous demandez spécifiquement sur map(), filter()et reduce(), mais je suppose que vous voulez savoir sur la programmation fonctionnelle en général. Ayant moi-même testé cela sur le problème du calcul des distances entre tous les points dans un ensemble de points, la programmation fonctionnelle (utilisant la starmapfonction du itertoolsmodule intégré) s'est avérée légèrement plus lente que les boucles for (prenant 1,25 fois plus longtemps, en fait). Voici l'exemple de code que j'ai utilisé:

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 version fonctionnelle est-elle plus rapide que la version procédurale?

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')
andreipmbcn
la source
2
Cela semble être une façon assez compliquée de répondre à cette question. Pouvez-vous le réduire pour qu'il soit plus logique?
Aaron Hall
2
@AaronHall Je trouve en fait la réponse d'andreipmbcn plutôt intéressante car c'est un exemple non trivial. Code avec lequel nous pouvons jouer.
Anthony Kong
@AaronHall, voulez-vous que je modifie le paragraphe de texte pour qu'il paraisse plus clair et plus simple, ou voulez-vous que je modifie le code?
andreipmbcn
9

J'ai écrit un script simple qui teste la vitesse et c'est ce que j'ai découvert. En fait, la boucle for était la plus rapide dans mon cas. Cela m'a vraiment surpris, regardez ci-dessous (calculait la somme des carrés).

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
alphiii
la source
Avec python 3.6.1, les différences ne sont pas si grandes; Réduire et Map descendent à 0,24 et la compréhension de la liste à 0,29. Car est plus élevé, à 0,18.
jjmerelo
L'élimination du intin le square_sum4rend également un peu plus rapide et juste un peu plus lent que la boucle for.
jjmerelo
6

J'ai modifié le code de @ Alisa et utilisé cProfilepour montrer pourquoi la compréhension de liste est plus rapide:

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

Voici les résultats:

=========================
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 MON HUMBLE AVIS:

  • reduceet mapsont en général assez lents. Non seulement cela, l'utilisation sumsur les itérateurs qui sont mapretournés est lente, par rapport à sumune liste
  • for_loop utilise append, qui est bien sûr lent dans une certaine mesure
  • la compréhension de la liste a non seulement passé le moins de temps à construire la liste, mais elle rend également sumbeaucoup plus rapide, contrairement àmap
tjysdsg
la source
5

En ajoutant une torsion à la réponse Alphii , en fait la boucle for serait la deuxième meilleure et environ 6 fois plus lente quemap

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

Les principaux changements ont été d'éliminer les sumappels lents , ainsi que les appels probablement inutiles int()dans le dernier cas. Mettre la boucle for et la carte dans les mêmes termes en fait tout à fait un fait. Rappelez-vous que les lambdas sont des concepts fonctionnels et ne devraient théoriquement pas avoir d'effets secondaires, mais, eh bien, ils peuvent avoir des effets secondaires comme l'ajout de a. Résultats dans ce cas avec Python 3.6.1, Ubuntu 14.04, processeur Intel (R) Core (TM) i7-4770 à 3,40 GHz

0:00:00.257703 #Reduce
0:00:00.184898 #For loop
0:00:00.031718 #Map
0:00:00.212699 #List comprehension
jjmerelo
la source
2
square_sum3 et square_sum4 sont incorrects. Ils ne donneront pas de somme. La réponse ci-dessous de @alisca chen est en fait correcte.
ShikharDua
3

J'ai réussi à modifier une partie du code de @ alpiii et j'ai découvert que la compréhension de liste est un peu plus rapide que la boucle for. Cela peut être causé par int(), ce n'est pas juste entre la compréhension de la liste et la boucle 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 #Reduce

0:00:00.089216 #For loop

0:00:00.101532 #Map

0:00:00.068916 #List comprehension
Alisca Chen
la source