def main():
for i in xrange(10**8):
pass
main()
Este trecho de código no Python é executado (Observação: o tempo é feito com a função de tempo no BASH no Linux.)
real 0m1.841s
user 0m1.828s
sys 0m0.012s
No entanto, se o loop for não for colocado em uma função,
for i in xrange(10**8):
pass
então ele roda por um tempo muito maior:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Por que é isso?
python
performance
profiling
benchmarking
cpython
thedoctar
fonte
fonte
Respostas:
Você pode perguntar por que é mais rápido armazenar variáveis locais do que globais. Este é um detalhe de implementação do CPython.
Lembre-se de que o CPython é compilado no bytecode, que o interpretador executa. Quando uma função é compilada, as variáveis locais são armazenadas em uma matriz de tamanho fixo ( não a
dict
) e os nomes das variáveis são atribuídos aos índices. Isso é possível porque você não pode adicionar dinamicamente variáveis locais a uma função. A recuperação de uma variável local é literalmente uma pesquisa de ponteiro na lista e um aumento de refcount noPyObject
que é trivial.Compare isso com uma pesquisa global (
LOAD_GLOBAL
), que é uma verdadeiradict
pesquisa envolvendo um hash e assim por diante. Aliás, é por isso que você precisa especificarglobal i
se deseja que seja global: se você atribuir a uma variável dentro de um escopo, o compilador emitiráSTORE_FAST
s para seu acesso, a menos que você o solicite.A propósito, as pesquisas globais ainda são bastante otimizadas. As pesquisas de atributos
foo.bar
são realmente lentas!Aqui está uma pequena ilustração sobre a eficiência variável local.
fonte
def foo_func: x = 5
,x
é local para uma função. O acessox
é local.foo = SomeClass()
,foo.bar
é o acesso ao atributo.val = 5
global é global. Quanto à velocidade local> global> atributo de acordo com o que eu li aqui. Então, acessandox
emfoo_func
é mais rápido, seguido deval
, seguido porfoo.bar
.foo.attr
não é uma pesquisa local porque, no contexto desta convo, estamos falando de pesquisas locais como uma pesquisa de uma variável que pertence a uma função.globals()
função. Se você quiser mais informações do que isso, talvez precise começar a procurar o código-fonte do Python. E CPython é apenas o nome para a implementação usual do Python - então você provavelmente já o está usando!Dentro de uma função, o bytecode é:
No nível superior, o bytecode é:
A diferença é que
STORE_FAST
é mais rápido (!) QueSTORE_NAME
. Isso ocorre porque em uma funçãoi
é local, mas no nível superior é global.Para examinar bytecode, use o
dis
módulo . Consegui desmontar a função diretamente, mas para desmontar o código de nível superior, tive que usar ocompile
builtin .fonte
global i
namain
função torna os tempos de execução equivalentes.locals()
, via ,inspect.getframe()
etc.). Procurar um elemento da matriz por um número inteiro constante é muito mais rápido do que pesquisar um ditado.Além dos tempos de armazenamento variáveis locais / globais, a previsão do código de operação torna a função mais rápida.
Como as outras respostas explicam, a função usa o
STORE_FAST
código de operação no loop. Aqui está o bytecode para o loop da função:Normalmente, quando um programa é executado, o Python executa cada código de operação um após o outro, acompanhando a pilha e pré-formando outras verificações no quadro da pilha após a execução de cada código de operação. A previsão de código de operação significa que, em certos casos, o Python é capaz de pular diretamente para o próximo código de operação, evitando parte dessa sobrecarga.
Nesse caso, toda vez que o Python vir
FOR_ITER
(a parte superior do loop), ele "prediz" queSTORE_FAST
é o próximo opcode que ele deve executar. O Python então espreita o próximo código de operação e, se a previsão estiver correta, salta diretamente paraSTORE_FAST
. Isso tem o efeito de espremer os dois códigos de operação em um único código de operação.Por outro lado, o
STORE_NAME
código de operação é usado no loop em nível global. O Python * não * faz previsões semelhantes quando vê esse opcode. Em vez disso, ele deve voltar ao topo do loop de avaliação, o que tem implicações óbvias para a velocidade com que o loop é executado.Para fornecer mais detalhes técnicos sobre essa otimização, aqui está uma citação do
ceval.c
arquivo (o "mecanismo" da máquina virtual do Python):Podemos ver no código fonte do
FOR_ITER
opcode exatamente onde a previsãoSTORE_FAST
é feita:A
PREDICT
função se expande para,if (*next_instr == op) goto PRED_##op
ou seja, apenas saltamos para o início do código de operação previsto. Nesse caso, pulamos aqui:A variável local está agora definida e o próximo opcode está pronto para execução. O Python continua pelo iterável até chegar ao fim, fazendo a previsão bem-sucedida a cada vez.
A página wiki do Python tem mais informações sobre como a máquina virtual do CPython funciona.
fonte
HAS_ARG
teste nunca ocorre (exceto quando o rastreamento de baixo nível é ativado no momento da compilação e do tempo de execução, o que nenhuma compilação normal faz), deixando apenas um salto imprevisível.PREDICT
macro está completamente desativada; em vez disso, a maioria dos casos termina em umaDISPATCH
ramificação diretamente. Mas nas CPUs de previsão de ramificação, o efeito é semelhante ao dePREDICT
, uma vez que ramificação (e previsão) é por código de operação, aumentando as chances de previsão de ramificação bem-sucedida.