As compreensões de lista e as funções funcionais são mais rápidas que "para loops"?

155

Em termos de desempenho em Python, é uma lista-compreensão, ou funções gosto map(), filter()e reduce()mais rápido do que um loop for? Por que, tecnicamente, eles são executados na velocidade C , enquanto o loop for é executado na velocidade da máquina virtual python ?

Suponha que em um jogo que estou desenvolvendo, preciso desenhar mapas complexos e enormes usando loops. Essa pergunta seria definitivamente relevante, pois se uma compreensão de lista, por exemplo, for realmente mais rápida, seria uma opção muito melhor para evitar atrasos (apesar da complexidade visual do código).

Ericson Willians
fonte

Respostas:

146

A seguir, são apresentadas diretrizes aproximadas e suposições fundamentadas com base na experiência. Você deve timeitcriar um perfil do seu caso de uso concreto para obter números concretos e esses números podem ocasionalmente discordar do abaixo.

Uma compreensão de lista geralmente é um pouco mais rápida que o forloop precisamente equivalente (que realmente cria uma lista), provavelmente porque não precisa procurar a lista e seu appendmétodo em todas as iterações. No entanto, uma compreensão de lista ainda faz um loop no nível de 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

Usar uma compreensão de lista no lugar de um loop que não cria uma lista, acumular absurdamente uma lista de valores sem sentido e depois jogá-la fora, geralmente é mais lento devido à sobrecarga de criar e estender a lista. A compreensão de lista não é mágica que é inerentemente mais rápida que um bom e antigo loop.

Quanto às funções funcionais de processamento de lista: Embora sejam escritas em C e provavelmente superem as funções equivalentes escritas em Python, elas não são necessariamente a opção mais rápida. É esperada alguma aceleração se a função também estiver escrita em C. Porém, na maioria dos casos, o uso de uma lambda(ou outra função Python), a sobrecarga da configuração repetida de quadros de pilha Python etc. consome qualquer economia. Simplesmente fazer o mesmo trabalho em linha, sem chamadas de função (por exemplo, uma compreensão da lista em vez de mapou filter) geralmente é um pouco mais rápido.

Suponha que em um jogo que estou desenvolvendo, preciso desenhar mapas complexos e enormes usando loops. Essa pergunta seria definitivamente relevante, pois se uma compreensão de lista, por exemplo, for realmente mais rápida, seria uma opção muito melhor para evitar atrasos (apesar da complexidade visual do código).

Provavelmente, se um código como esse já não for rápido o suficiente quando escrito em bom Python não "otimizado", nenhuma micro otimização no nível do Python o tornará rápido o suficiente e você deve começar a pensar em mudar para C. Embora extenso Quando as micro otimizações costumam acelerar consideravelmente o código Python, existe um limite baixo (em termos absolutos) para isso. Além disso, mesmo antes de atingir esse limite, torna-se simplesmente mais econômico (15% de aceleração vs. 300% de aceleração com o mesmo esforço) morder a bala e escrever um pouco de C.


fonte
25

Se você verificar as informações em python.org , poderá ver este resumo:

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

Mas você realmente deve ler o artigo acima em detalhes para entender a causa da diferença de desempenho.

Eu também sugiro fortemente que você deve cronometrar seu código usando timeit . No final do dia, pode haver uma situação em que, por exemplo, você precise interromper o forciclo quando uma condição for atendida. Pode ser mais rápido do que descobrir o resultado ligando map.

Anthony Kong
fonte
17
Embora essa página seja uma boa leitura e parcialmente relacionada, apenas citar esses números não é útil, possivelmente até enganoso.
1
Isso não dá nenhuma indicação do que você está cronometrando. O desempenho relativo variará bastante, dependendo do conteúdo do loop / listcomp / map.
User2357112 suporta Monica
@delnan eu concordo. Modifiquei minha resposta para solicitar ao OP que leia a documentação para entender a diferença de desempenho.
Anthony Kong
@ user2357112 Você precisa ler a página wiki que eu vinculei para o contexto. Eu postei para referência do OP.
Anthony Kong
13

Você pergunta especificamente sobre map(), filter()e reduce(), mas presumo que você queira saber sobre programação funcional em geral. Tendo testado isso sozinho no problema de calcular distâncias entre todos os pontos dentro de um conjunto de pontos, a programação funcional (usando a starmapfunção do itertoolsmódulo embutido ) mostrou-se um pouco mais lenta que a dos loops for-loops (demorando 1,25 vezes mais, em facto). Aqui está o código de exemplo que eu usei:

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

A versão funcional é mais rápida que a versão processual?

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
fonte
2
Parece uma maneira bastante complicada de responder a essa pergunta. Você pode reduzi-lo para que faça mais sentido?
Aaron Hall
2
@AaronHall Na verdade, acho a resposta de andreipmbcn bastante interessante porque é um exemplo não trivial. Código com o qual podemos jogar.
Anthony Kong
@AaronHall, você quer que eu edite o parágrafo do texto para que pareça mais claro e direto, ou deseja que eu edite o código?
precisa saber é o seguinte
9

Escrevi um script simples que testa a velocidade e foi isso que descobri. Na verdade, o loop foi o mais rápido no meu caso. Isso realmente me surpreendeu, confira abaixo (estava calculando a soma dos quadrados).

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
fonte
No python 3.6.1, as diferenças não são tão grandes; Reduzir e Mapa diminuem para 0,24 e listam a compreensão para 0,29. Pois é maior, em 0,18.
jjmerelo
Eliminar o intin square_sum4também o torna um pouco mais rápido e um pouco mais lento que o loop for.
jjmerelo
6

I modificado @ código de Alisa e usado cProfilepara mostrar porque compreensão da lista é mais rápido:

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

Aqui estão os resultados:

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

NA MINHA HUMILDE OPINIÃO:

  • reducee mapsão geralmente bem lentos. Além disso, o uso sumnos iteradores que mapretornaram é lento, comparado suma uma lista
  • for_loop usa append, o que obviamente é lento até certo ponto
  • A compreensão da lista não apenas gastou menos tempo construindo a lista, como também torna summuito mais rápido, em contraste commap
tjysdsg
fonte
5

Adicionando um toque à resposta Alphii , na verdade o loop for seria o segundo melhor e cerca de 6 vezes mais lento que omap

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

As principais mudanças foram eliminar as sumchamadas lentas e as provavelmente desnecessárias int()no último caso. Colocar o loop e o mapa for nos mesmos termos torna isso realmente verdade. Lembre-se de que lambdas são conceitos funcionais e, teoricamente, não devem ter efeitos colaterais, mas, bem, eles podem ter efeitos colaterais como adicionar a a. Resultados neste caso com Python 3.6.1, Ubuntu 14.04, CPU Intel (R) Core (TM) i7-4770 a 3.40GHz

0:00:00.257703 #Reduce
0:00:00.184898 #For loop
0:00:00.031718 #Map
0:00:00.212699 #List comprehension
jjmerelo
fonte
2
square_sum3 e square_sum4 estão incorretos. Eles não vão dar soma. A resposta abaixo de @alisca chen está realmente correta.
ShikharDua 30/10/19
3

Consegui modificar alguns códigos do @ alpiii e descobri que a compreensão da lista é um pouco mais rápida do que o loop for. Pode ser causado por int(), não é justo entre a compreensão da lista e o loop 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
fonte