Usar a insert
função de uma lista é muito mais lento do que obter o mesmo efeito usando a atribuição de fatia:
> python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)"
100000 loops, best of 5: 19.2 usec per loop
> python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]"
100000 loops, best of 5: 6.78 usec per loop
(Observe que a=[]
é apenas a configuração, portanto, a
começa vazio, mas depois aumenta para 100.000 elementos.)
No começo, pensei que talvez fosse a consulta de atributo ou a sobrecarga de chamada de função, mas inserir no final mostra que isso é insignificante:
> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop
Por que a função "inserir elemento único" presumivelmente mais simples é muito mais lenta?
Também posso reproduzi-lo em repl.it :
from timeit import repeat
for _ in range(3):
for stmt in 'a.insert(0,0)', 'a[0:0]=[0]', 'a.insert(-1,0)':
t = min(repeat(stmt, 'a=[]', number=10**5))
print('%.6f' % t, stmt)
print()
# Example output:
#
# 4.803514 a.insert(0,0)
# 1.807832 a[0:0]=[0]
# 0.012533 a.insert(-1,0)
#
# 4.967313 a.insert(0,0)
# 1.821665 a[0:0]=[0]
# 0.012738 a.insert(-1,0)
#
# 5.694100 a.insert(0,0)
# 1.899940 a[0:0]=[0]
# 0.012664 a.insert(-1,0)
Eu uso o Python 3.8.1 de 32 bits no Windows 10 de 64 bits.
O repl.it usa o Python 3.8.1 de 64 bits no Linux de 64 bits.
python
performance
Estouro de pilha
fonte
fonte
a=[]; a[0:0]=[0]
faz o mesmo quea=[]; a[100:200]=[0]
a=[1,2,3];a[100:200]=[4]
está anexando4
ao final da listaa
interessante.a=[]; a[0:0]=[0]
ou quea[0:0]=[0]
faz o mesmo quea[100:200]=[0]
...Respostas:
Eu acho que é provavelmente apenas que se esqueceram de usar
memmove
nolist.insert
. Se você der uma olhada no códigolist.insert
usado para mudar elementos, poderá ver que é apenas um loop manual:enquanto
list.__setitem__
no caminho de atribuição de fatia usamemmove
:memmove
normalmente, há muita otimização, como tirar proveito das instruções SSE / AVX.fonte
-O3
auto-vetorização ativada, esse loop manual pode compilar com eficiência. Mas, a menos que o compilador reconheça o loop como sendo um memmove e o compile em uma chamada real paramemmove
, ele poderá aproveitar apenas as extensões do conjunto de instruções ativadas no momento da compilação. (Ótimo se você estiver construindo o seu próprio-march=native
, não muito para binários de distribuição criados com a linha de base). E o GCC não desenrola os loops por padrão, a menos que você use o PGO (-fprofile-generate
/ run /...-use
)memmove
chamada real , ele poderá tirar proveito de todas as extensões presentes no tempo de execução?cpuid
). O mesmo para várias outras funções mem / str. Assim, as distros podem ser compiladas apenas-O2
para criar binários executados em qualquer lugar, mas pelo menos o memcpy / memmove use um loop AVX desenrolado carregando / armazenando 32 bytes por instrução. (Ou até o AVX512 nos poucos processadores em que é uma boa ideia; acho que apenas Xeon Phi.)memmove
versões estão lá na libc.so, a biblioteca compartilhada. Para cada função, o despacho acontece uma vez, durante a resolução do símbolo (ligação antecipada ou na primeira chamada com ligação lenta tradicional). Como eu disse, apenas sobrecarrega / conecta como a vinculação dinâmica acontece, não envolvendo a função em si. (especificamente pelo mecanismo ifunc do GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Relacionado: para memset, a escolha usual em CPUs modernas é__memset_avx2_unaligned_erms
ver esta