Por que o máximo é mais lento do que a classificação?

92

Descobri que maxé mais lento do que a sortfunção em Python 2 e 3.

Python 2

$ python -m timeit -s 'import random;a=range(10000);random.shuffle(a)' 'a.sort();a[-1]'
1000 loops, best of 3: 239 usec per loop
$ python -m timeit -s 'import random;a=range(10000);random.shuffle(a)' 'max(a)'        
1000 loops, best of 3: 342 usec per loop

Python 3

$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'a.sort();a[-1]'
1000 loops, best of 3: 252 usec per loop
$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'max(a)'
1000 loops, best of 3: 371 usec per loop

Por que é max ( O(n)) mais lento do que o sortfunction ( O(nlogn))?

WeizhongTu
fonte
3
Você executou a análise do Python 2 uma vez e o código do Python 3 é exatamente o mesmo.
apagar
9
a.sort()funciona no local. Tentesorted(a)
Andrea Corbellini
Se você consertou, poste de volta o que você fez para consertar, por favor.
Pretzel
4
@Pretzel OP significa que a postagem foi editada, não que o problema foi corrigido.
apagar
2
@WeizhongTu, mas sortclassifica e aé classificado para sempre
njzk2

Respostas:

125

Você deve ter muito cuidado ao usar o timeitmódulo em Python.

python -m timeit -s 'import random;a=range(10000);random.shuffle(a)' 'a.sort();a[-1]'

Aqui, o código de inicialização é executado uma vez para produzir uma matriz aleatória a. Em seguida, o resto do código é executado várias vezes. Na primeira vez, ele classifica a matriz, mas a cada duas vezes você chama o método de classificação em uma matriz já classificada. Apenas o tempo mais rápido é retornado, então você está realmente cronometrando quanto tempo leva para o Python classificar um array já classificado.

Parte do algoritmo de classificação do Python é detectar quando o array já está parcial ou completamente classificado. Quando completamente classificado, ele simplesmente tem que fazer uma varredura no array para detectar isso e então para.

Se em vez disso você tentou:

python -m timeit -s 'import random;a=range(100000);random.shuffle(a)' 'sorted(a)[-1]'

então a classificação acontece em cada loop de tempo e você pode ver que o tempo para classificar uma matriz é de fato muito mais longo do que apenas encontrar o valor máximo.

Edit: a resposta de @ skyking explica a parte que deixei sem explicação: a.sort()sabe que está trabalhando em uma lista, então pode acessar diretamente os elementos. max(a)funciona em qualquer iterável arbitrário, portanto, precisa usar a iteração genérica.

Duncan
fonte
10
Boa pegada. Nunca percebi que o estado do interpretador é mantido durante a execução do código. Agora eu me pergunto quantos benchmarks defeituosos eu produzi no passado. : -}
Frerich Raabe
1
Isso era óbvio para mim. Mas observe que mesmo se você classificar um array já classificado, você deve verificar todos os elementos. O que dá tanto trabalho quanto obter o máximo ... Para mim, isso parece uma meia resposta.
Karoly Horvath
2
@KarolyHorvath, você está correto. Acho que @skyking obteve a outra metade da resposta: a.sort()sabe que está trabalhando em uma lista, então pode acessar diretamente os elementos. max(a)funciona em uma sequência arbitrária para não usar iteração genérica.
Duncan
1
@KarolyHorvath talvez a previsão de branch possa explicar por que classificar repetidamente um array classificado é mais rápido: stackoverflow.com/a/11227902/4600
marcospereira
1
@JuniorCompressor listsort.txtexplica "Ele tem desempenho sobrenatural em muitos tipos de arrays parcialmente ordenados (menos de lg (N!) Comparações necessárias e tão poucas quanto N-1)" e então continua explicando todos os tipos de otimizações sangrentas. Suponho que ele pode fazer muitas suposições que maxnão podem, ou seja, a classificação não é assintoticamente mais rápida.
Frerich Raabe
87

Em primeiro lugar, observe que max()usa o protocolo iterador , enquanto list.sort()usa código ad-hoc . Claramente, usar um iterador é uma sobrecarga importante, é por isso que você está observando essa diferença nos tempos.

No entanto, além disso, seus testes não são justos. Você está concorrendo a.sort()na mesma lista mais de uma vez. O algoritmo usado pelo Python é projetado especificamente para ser rápido para dados já (parcialmente) classificados. Seus testes estão dizendo que o algoritmo está fazendo seu trabalho bem.

Estes são testes controlados:

$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'max(a[:])'
1000 loops, best of 3: 227 usec per loop
$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'a[:].sort()'
100 loops, best of 3: 2.28 msec per loop

Aqui estou sempre criando uma cópia da lista. Como você pode ver, a ordem de magnitude dos resultados é diferente: microssegundos vs milissegundos, como seria de se esperar.

E lembre-se: big-Oh especifica um limite superior! O limite inferior para o algoritmo de classificação do Python é Ω ( n ). Ser O ( n log n ) não implica automaticamente que cada execução leva um tempo proporcional a n log n . Nem mesmo implica que precisa ser mais lento do que um algoritmo O ( n ), mas isso é outra história. O que é importante entender é que, em alguns casos favoráveis, um algoritmo O ( n log n ) pode ser executado em tempo O ( n ) ou menos.

Andrea Corbellini
fonte
31

Isto poderia ser porque l.sorté um membro da listenquanto maxé uma função genérica. Isso significa que l.sortpode contar com a representação interna de listwhile maxterá que passar por protocolo iterador genérico.

Isso faz com que a busca de cada elemento l.sortseja mais rápida do que a busca de cada elemento max.

Presumo que, se você usar sorted(a), obterá o resultado mais lento do que max(a).

skyking
fonte
5
Essa suposição está a apenas uma linha de distância para se tornar mais concreta. Sem questionar o seu conhecimento, apenas que tal acréscimo é trivial para a demonstração de quem não o conhece.
Reti43 de
Você está correto que sorted(a)é mais lento do que max(a). Não é de surpreender que seja quase a mesma velocidade que a.sort(), mas sua conjectura quanto ao motivo pelo qual não é - é porque o OP cometeu um erro em seus testes, conforme apontado na resposta aceita.
martineau de
A questão é que existe a possibilidade de que o protocolo iterador genérico tenha sobrecarga suficiente para compensar o log(n)fator de complexidade. Ou seja, um O(n)algoritmo só tem garantia de ser mais rápido do que um O(nlogn)algoritmo suficientemente grande n(por exemplo, porque o tempo para cada operação pode ser diferente entre os algoritmos - as nlognetapas rápidas podem ser mais rápidas do que as nlentas). Exatamente onde o ponto de equilíbrio é não considerado neste caso (mas deve-se estar ciente de que o log nfator não é um fator muito grande para algo pequeno n).
skyking em