web-dev-qa-db-fra.com

Les compréhensions de liste et les fonctions fonctionnelles sont-elles plus rapides que "pour les boucles"?

En termes de performances en Python, une compréhension par liste, ou des fonctions telles que map (), filter () et reduction () sont-elles plus rapides qu'une boucle for? Pourquoi, techniquement, ils "tournent à la vitesse C", alors que "la boucle for tourne à la vitesse de la machine virtuelle python" ?.

Supposons que dans un jeu que je développe, je dois dessiner des cartes complexes et immenses à l'aide de boucles for. Cette question serait tout à fait pertinente, car si une compréhension de liste, par exemple, est effectivement plus rapide, ce serait une bien meilleure option pour éviter les retards (malgré la complexité visuelle du code).

119
Ericson Willians

Ce qui suit sont des directives approximatives et des suppositions éclairées basées sur l'expérience. Vous devez timeit ou définir votre cas d'utilisation concret pour obtenir des chiffres précis. Ces chiffres peuvent parfois être en désaccord avec ce qui suit.

Une compréhension de liste est généralement un peu plus rapide que la boucle for (qui construit en fait une liste), qui est précisément équivalente, probablement parce qu’elle n’a pas à rechercher la liste et sa méthode append à chaque fois. itération. Cependant, une compréhension de liste effectue toujours une boucle de niveau 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 liste de compréhension à la place d'une boucle qui ne construit pas de liste , accumule de manière absurde une liste de valeurs sans signification et jette ensuite la liste, souvent plus lent en raison de la surcharge liée à la création et à l'extension de la liste. Les compréhensions de liste ne sont pas une magie intrinsèquement plus rapide qu'une bonne vieille boucle.

En ce qui concerne les fonctions de traitement de listes fonctionnelles: bien qu’elles soient écrites en C et surpassent probablement les fonctions équivalentes écrites en Python, elles sont et non l’option la plus rapide. Une certaine accélération est attendue if la fonction est écrite en C aussi. Mais dans la plupart des cas utilisant une fonction lambda (ou une autre fonction Python), la surcharge de la configuration répétée de Python images de pile, etc. entraîne des économies. Effectuer simplement le même travail en ligne, sans appel de fonction (par exemple, une compréhension de liste au lieu de map ou filter) est souvent légèrement plus rapide.

Supposons que dans un jeu que je développe, je dois dessiner des cartes complexes et immenses à l'aide de boucles for. Cette question serait tout à fait pertinente, car si une compréhension de liste, par exemple, est effectivement plus rapide, ce serait une bien meilleure option pour éviter les retards (malgré la complexité visuelle du code).

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

115
user395760

Si vous vérifiez le info 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 suggère également fortement que vous devriez chronométrer votre code en utilisant timeit . À la fin de la journée, il peut arriver que vous deviez, par exemple, sortir de la boucle for lorsqu'une condition est remplie. Cela pourrait être plus rapide que de connaître le résultat en appelant map.

16
Anthony Kong

Vous posez des questions spécifiques sur map (), filter () et reduction (), mais je suppose que vous voulez en savoir plus sur la programmation fonctionnelle. Ayant moi-même testé cela sur le problème des distances de calcul entre tous les points dans un ensemble de points, la programmation fonctionnelle (utilisant la fonction starmap du module intégré d'itertools) s'est avérée légèrement plus lente que les boucles for (prenant 1,25 fois plus longtemps , En réalité). 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')
12
andreipmbcn

J'ai écrit un script simple qui teste la vitesse et c'est ce que j'ai découvert. En fait, la boucle était la plus rapide dans mon cas. Cela m'a vraiment surpris, voir 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

7
alphiii

En ajoutant une torsion à Alphii answer , la boucle for serait en fait la deuxième meilleure solution et environ 6 fois plus lente que 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])

Les principales modifications consistent à éliminer les appels lents sum ainsi que les _ probablement inutiles int() dans le dernier cas. Mettre la boucle et la carte dans les mêmes termes en fait un fait, en fait. Rappelez-vous que les lambdas sont des concepts fonctionnels et qu’ils ne devraient théoriquement pas avoir d’effets secondaires, mais bien, ils peuvent avoir des effets secondaires tels que l’ajout à a . Résultats dans ce cas avec Python 3.6.1, Ubuntu 14.04, CPU i7-4770 Intel (R) Core (TM) à 3,40 GHz

0:00:00.257703
0:00:00.184898
0:00:00.031718
0:00:00.212699
5
jjmerelo

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

0: 00: 00.089216

0: 00: 00.101532

0: 00: 00.068916

0
Alisca Chen