Copiar uma range(10**6)
lista embaralhada dez vezes leva cerca de 0,18 segundos: (são cinco execuções)
0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451
Copiar a lista não embaralhada dez vezes leva cerca de 0,05 segundos:
0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184
Este é meu código de teste:
from timeit import timeit
import random
a = range(10**6)
random.shuffle(a) # Remove this for the second test.
a = list(a) # Just an attempt to "normalize" the list.
for _ in range(5):
print timeit(lambda: list(a), number=10)
Também tentei copiar com a[:]
, os resultados foram semelhantes (ou seja, grande diferença de velocidade)
Por que a grande diferença de velocidade? Eu sei e entendo a diferença de velocidade no famoso Por que é mais rápido processar um array ordenado do que um array não ordenado? exemplo, mas aqui meu processamento não tem decisões. É apenas copiar cegamente as referências dentro da lista, não?
Estou usando o Python 2.7.12 no Windows 10.
Edit: Python 3.5.2 experimentado também agora, os resultados foram quase os mesmos (embaralhado consistentemente em torno de 0,17 segundos, não embaralhado consistentemente em torno de 0,05 segundos). Aqui está o código para isso:
a = list(range(10**6))
random.shuffle(a)
a = list(a)
for _ in range(5):
print(timeit(lambda: list(a), number=10))
fonte
0.25
em cada iteração de cada um dos testes. Portanto, na minha plataforma, a ordem importa.Respostas:
O interessante é que isso depende da ordem em que os inteiros são criados pela primeira vez. Por exemplo, em vez de
shuffle
criar uma sequência aleatória comrandom.randint
:Isso é tão rápido quanto copiar o seu
list(range(10**6))
(primeiro e rápido exemplo).No entanto, quando você embaralha - então seus inteiros não estão mais na ordem em que foram criados, é o que o torna lento.
Um intermezzo rápido:
Py_INCREF
emlist_slice
), de modo que o Python realmente precisa ir para onde o objeto está. Ele não pode simplesmente copiar a referência.Portanto, ao copiar sua lista, você obtém cada item dessa lista e os coloca "como estão" na nova lista. Quando seu próximo item foi criado, logo após o atual, há uma boa chance (não há garantia!) De que ele seja salvo próximo a ele na pilha.
Vamos supor que sempre que seu computador carregar um item no cache, ele também carregará os
x
próximos itens na memória (localidade do cache). Então seu computador pode realizar o incremento da contagem de referência parax+1
itens no mesmo cache!Com a sequência embaralhada, ele ainda carrega os próximos itens da memória, mas esses não são os próximos da lista. Portanto, ele não pode realizar o incremento da contagem de referência sem "realmente" procurar o próximo item.
TL; DR: A velocidade real depende do que aconteceu antes da cópia: em que ordem esses itens foram criados e em que ordem estão na lista.
Você pode verificar isso olhando para
id
:Só para mostrar um pequeno trecho:
Portanto, esses objetos estão realmente "próximos uns dos outros na pilha". Com
shuffle
eles não são:O que mostra que eles não estão realmente próximos um do outro na memória:
Nota importante:
Eu não pensei nisso sozinho. A maioria das informações pode ser encontrada na postagem do blog de Ricky Stewart .
Esta resposta é baseada na implementação "oficial" do CPython do Python. Os detalhes em outras implementações (Jython, PyPy, IronPython, ...) podem ser diferentes. Obrigado @ JörgWMittag por apontar isso .
fonte
list_slice
e na linha 453 você pode ver aPy_INCREF(v);
chamada que precisa acessar o objeto alocado no heap.a = [0] * 10**7
(acima de 10 ** 6 porque era muito instável), que é ainda mais rápido do que usara = range(10**7)
(por um fator de cerca de 1,25). Claramente porque isso é ainda melhor para armazenamento em cache.[0,1,2,3]*((10**6) // 4)
é tão rápido quantoa = [0] * 10**6
. No entanto, com inteiros de 0-255, há outro fato chegando: eles são internados, portanto, com eles a ordem de criação (dentro do seu script) não é mais importante - porque eles são criados quando você inicia o python.Quando você embaralha os itens da lista, eles têm pior localidade de referência, levando a um pior desempenho do cache.
Você pode pensar que copiar a lista apenas copia as referências, não os objetos, portanto, suas localizações no heap não devem importar. No entanto, a cópia ainda envolve acessar cada objeto para modificar o refcount.
fonte
Como explicado por outros, não se trata apenas de copiar as referências, mas também aumenta as contagens de referência dentro dos objetos e, portanto, os objetos são acessados e o cache desempenha um papel.
Aqui, eu só quero adicionar mais experimentos. Não tanto sobre embaralhado versus não embaralhado (onde acessar um elemento pode perder o cache, mas obter os seguintes elementos no cache para que sejam atingidos). Mas sobre elementos de repetição, onde acessos posteriores do mesmo elemento podem atingir o cache porque o elemento ainda está no cache.
Testando uma faixa normal:
Uma lista do mesmo tamanho, mas com apenas um elemento repetido várias vezes, é mais rápida porque atinge o cache o tempo todo:
E não parece importar qual é o número:
Curiosamente, fica ainda mais rápido quando, em vez disso, repito os mesmos dois ou quatro elementos:
Acho que algo não gosta que o mesmo contador único esteja sempre aumentando. Talvez um pouco pipeline pare porque cada aumento precisa esperar pelo resultado do aumento anterior, mas esse é um palpite.
De qualquer forma, tentando fazer isso para um número ainda maior de elementos repetidos:
A saída (a primeira coluna é o número de elementos diferentes, para cada um testo três vezes e depois tiro a média):
Portanto, de cerca de 2,8 segundos para um único elemento (repetido), ele cai para cerca de 2,2 segundos para 2, 4, 8, 16, ... elementos diferentes e permanece em cerca de 2,2 segundos até os cem mil. Eu acho que isso usa meu cache L2 (4 × 256 KB, eu tenho um i7-6700 ).
Depois de alguns passos, o tempo sobe para 3,5 segundos. Eu acho que isso usa uma mistura de meu cache L2 e meu cache L3 (8 MB) até que esteja "esgotado" também.
No final, ele fica em torno de 3,5 segundos, acho que porque meus caches não ajudam mais com os elementos repetidos.
fonte
Antes do embaralhamento, quando alocado no heap, os objetos de índice adjacentes são adjacentes na memória e a taxa de acerto de memória é alta quando acessada; após o embaralhamento, o objeto do índice adjacente da nova lista não está na memória. Adjacente, a taxa de acerto é muito baixa.
fonte