Python é lento. Porque

Recentemente, pode-se observar a crescente popularidade da linguagem de programação Python. É usado no DevOps, na análise de dados, no desenvolvimento da Web, no campo da segurança e em outros campos. Mas aqui está a velocidade ... Não há nada para se gabar dessa linguagem aqui. O autor do material, cuja tradução publicamos hoje, decidiu descobrir os motivos da lentidão do Python e encontrar meios de acelerá-lo.



Disposições Gerais


Como o Java, em termos de desempenho, se relaciona com C ou C ++? Como comparar C # e Python? As respostas a essas perguntas dependem muito do tipo de aplicação analisada pelo pesquisador. Não existe um benchmark perfeito, mas, estudando o desempenho de programas escritos em diferentes idiomas, o jogo Computer Benchmarks de benchmarks pode ser um bom ponto de partida .

Refiro-me ao jogo de benchmarks de linguagem de computador por mais de dez anos. O Python, em comparação com outras linguagens, como Java, C #, Go, JavaScript, C ++, é um dos mais lentos . Isso inclui linguagens que usam compilação JIT (C #, Java) e compilação AOT (C #, C ++), além de linguagens interpretadas como JavaScript.

Aqui, gostaria de observar que, quando digo “Python”, quero dizer a implementação de referência do interpretador Python - CPython. Neste material, abordaremos suas outras implementações. Na verdade, aqui eu quero encontrar a resposta para a pergunta de por que o Python leva de 2 a 10 vezes mais tempo do que outras linguagens para resolver problemas comparáveis ​​e se isso pode ser feito mais rapidamente.

Aqui estão algumas teorias básicas tentando explicar por que o Python é lento:

  • A razão para isso é o GIL (Global Interpreter Lock, Global Interpreter Lock).
  • O motivo é que o Python é uma linguagem interpretada e não compilada.
  • O motivo é a digitação dinâmica.

Analisaremos essas idéias e tentaremos encontrar a resposta para a pergunta do que tem maior efeito no desempenho dos aplicativos Python.

Gil


Computadores modernos têm processadores com vários núcleos e, às vezes, são encontrados sistemas multiprocessadores. Para usar todo esse poder de computação, o sistema operacional usa estruturas de baixo nível chamadas threads, enquanto processos (por exemplo, o processo do navegador Chrome) podem iniciar muitos threads e usá-los de acordo. Como resultado, por exemplo, se um processo precisa especialmente de recursos do processador, sua execução pode ser dividida entre vários núcleos, o que permite que a maioria dos aplicativos resolva as tarefas que enfrentam mais rapidamente.

Por exemplo, meu navegador Chrome, no momento em que escrevo isso, tem 44 threads abertos. Deve-se ter em mente que a estrutura e a API do sistema para trabalhar com fluxos variam nos sistemas operacionais baseados em Posix (Mac OS, Linux) e na família de sistemas operacionais Windows. O sistema operacional também planeja threads.

Se você não conheceu a programação multiencadeada antes, agora precisa se familiarizar com os chamados bloqueios (bloqueios). O significado de bloqueios é que eles permitem garantir esse comportamento do sistema quando, em um ambiente multithread, por exemplo, quando uma determinada variável na memória é alterada, vários threads não conseguem acessar a mesma área de memória (para leitura ou alteração).

Quando o intérprete CPython cria as variáveis, ele aloca memória e conta o número de referências existentes para essas variáveis. Esse conceito é conhecido como contagem de referência. Se o número de links for igual a zero, a parte correspondente da memória será liberada. É por isso que, por exemplo, a criação de variáveis ​​"temporárias", digamos, no escopo de loops, não leva a um aumento excessivo na quantidade de memória consumida pelo aplicativo.

A parte mais interessante começa quando vários threads compartilham as mesmas variáveis, e o principal problema aqui é como exatamente o CPython realiza a contagem de referência. É aqui que a ação do “bloqueio global de intérpretes” aparece, que controla cuidadosamente a execução dos encadeamentos.

Um intérprete pode executar apenas uma operação por vez, independentemente de quantos threads existem no programa.

▍Como o GIL afeta o desempenho dos aplicativos Python?


Se tivermos um aplicativo de thread único em execução no mesmo processo de interpretador Python, o GIL não afetará o desempenho de nenhuma maneira. Se, por exemplo, nos livrarmos do GIL, não notaremos nenhuma diferença no desempenho.

Se, na estrutura de um processo de intérprete Python, for necessário implementar o processamento paralelo de dados usando mecanismos de multithreading, e os fluxos usados ​​usarão intensivamente o subsistema de E / S (por exemplo, se eles trabalham com uma rede ou com um disco), será possível observar as consequências de como o GIL gerencia threads. Aqui está o que parece no caso de usar dois threads, carregando intensivamente os processos.


Visualização GIL (retirada daqui )

Se você possui um aplicativo da Web (por exemplo, com base na estrutura do Django) e usa o WSGI, cada solicitação do aplicativo da Web será atendida por um processo separado de intérprete do Python, ou seja, temos apenas 1 bloqueio de solicitação. Como o interpretador Python é iniciado lentamente, em algumas implementações do WSGI, existe o chamado "modo daemon", ao usar os processos do intérprete mantidos em condições de trabalho, o que permite ao sistema atender solicitações mais rapidamente.

▍Como se comportam outros intérpretes Python?


O PyPy possui um GIL, geralmente é mais de 3 vezes mais rápido que o CPython.

Não há GIL no Jython, porque os threads do Python no Jython são representados como threads do Java. Esses encadeamentos usam os recursos de gerenciamento de memória da JVM.

▍Como o controle de fluxo é organizado em JavaScript?


Se falamos de JavaScript, antes de tudo, deve-se notar que todos os mecanismos JS usam o algoritmo de coleta de lixo de marcação e varredura . Como já mencionado, o principal motivo para usar o GIL é o algoritmo de gerenciamento de memória usado no CPython.

O JavaScript não possui um GIL; no entanto, o JS é uma linguagem de thread único; portanto, ele não precisa desse mecanismo. Em vez da execução de código paralelo, o JavaScript usa técnicas de programação assíncrona com base em um loop de eventos, promessas e retornos de chamada. O Python tem algo semelhante fornecido pelo módulo asyncio .

Python - linguagem interpretada


Ouvi muitas vezes que o fraco desempenho do Python se deve ao fato de ser uma linguagem interpretada. Tais declarações são baseadas em uma simplificação grosseira de como o CPython realmente funciona. Se, no terminal, você digitar um comando como python myscript.py , o CPython iniciará uma longa sequência de ações, que consiste na leitura, análise lexical, análise, compilação, interpretação e execução de código de script. Se você estiver interessado nos detalhes, dê uma olhada neste material.

Para nós, ao considerar esse processo, é especialmente importante que aqui, no estágio de compilação, seja criado um arquivo .pyc e uma sequência de bytecodes seja gravada no arquivo no diretório __pycache__/ , usado em Python 3 e Python 2)

Isso se aplica não apenas aos scripts que escrevemos, mas também ao código importado, incluindo módulos de terceiros.

Como resultado, na maioria das vezes (a menos que você escreva o código que é executado apenas uma vez), o Python executará o bytecode finalizado. Comparando isso com o que acontece em Java e C #, verifica-se que o código Java é compilado na "Linguagem Intermediária", e a máquina virtual Java lê o bytecode e executa sua compilação JIT no código da máquina. O .NET CIL de "idioma intermediário" (que é o mesmo que o .NET Common-Language-Runtime, CLR) usa a compilação JIT para navegar para o código da máquina.

Como resultado, tanto em Java quanto em C #, é usada alguma "linguagem intermediária" e mecanismos semelhantes estão presentes. Por que, então, o Python mostra benchmarks muito piores que Java e C # se todas essas linguagens usam máquinas virtuais e algum tipo de bytecode? Primeiro de tudo, devido ao fato de a compilação JIT ser usada em .NET e Java.

A compilação JIT (compilação Just In Time, compilação dinâmica ou pontual) requer uma linguagem intermediária para permitir a divisão do código em fragmentos (quadros). Os sistemas de compilação AOT (compilação Antecipada, compilação antes da execução) são projetados para que o código esteja totalmente operacional antes do início da interação desse código com o sistema.

Por si só, o uso do JIT não acelera a execução do código, pois alguns fragmentos do código de bytes são executados, como no Python. No entanto, o JIT permite executar otimizações de código durante a execução. Um bom otimizador de JIT é capaz de identificar as partes mais carregadas do aplicativo (essa parte do aplicativo é chamada de "hot spot") e otimizar os fragmentos de código correspondentes, substituindo-os por opções otimizadas e mais produtivas do que aquelas usadas anteriormente.

Isso significa que, quando um determinado aplicativo executa certas ações repetidamente, essa otimização pode acelerar significativamente a execução de tais ações. Além disso, lembre-se de que Java e C # são linguagens fortemente tipadas, para que o otimizador possa fazer mais suposições sobre código que podem ajudar a melhorar o desempenho do programa.

Existe um compilador JIT no PyPy e, como já mencionado, essa implementação do interpretador Python é muito mais rápida que o CPython. Informações sobre como comparar diferentes intérpretes Python podem ser encontradas neste artigo.

▍ Por que o CPython não está usando um compilador JIT?


Os compiladores JIT também têm desvantagens. Uma delas é a hora do lançamento. O CPython já inicia de forma relativamente lenta e o PyPy é 2-3 vezes mais lento que o CPython. O tempo de longo prazo da JVM também é um fato conhecido. O CLR .NET contorna esse problema iniciando durante a inicialização do sistema, mas deve-se observar que o CLR e o sistema operacional que executa o CLR são desenvolvidos pela mesma empresa.

Se você possui um processo Python em execução há muito tempo, enquanto nesse processo existe um código que pode ser otimizado, uma vez que contém seções muito usadas, você deve procurar seriamente um intérprete que tenha um compilador JIT.

No entanto, o CPython é uma implementação do interpretador Python de uso geral. Portanto, se você estiver desenvolvendo, usando o Python, um aplicativo de linha de comando, a necessidade de uma longa espera para o compilador JIT iniciar cada vez que esse aplicativo for iniciado diminuirá bastante o trabalho.

O CPython está tentando fornecer suporte para o maior número possível de casos de uso do Python. Por exemplo, existe a possibilidade de conectar o compilador JIT ao Python, no entanto, o projeto que implementa essa ideia não está se desenvolvendo muito ativamente.

Como resultado, podemos dizer que, se você estiver usando o Python para escrever um programa cujo desempenho possa melhorar ao usar o compilador JIT, use o interpretador PyPy.

Python é uma linguagem de tipo dinâmico


Nas linguagens de tipo estaticamente, ao declarar variáveis, você deve especificar seus tipos. Entre essas linguagens, podemos citar C, C ++, Java, C #, Go.

Nas linguagens digitadas dinamicamente, o conceito de um tipo de dados tem o mesmo significado, mas o tipo de uma variável é dinâmico.

 a = 1 a = "foo" 

Neste exemplo mais simples, o Python cria primeiro a primeira variável a , depois a segunda com o mesmo nome do tipo str e libera a memória que foi alocada para a primeira variável a .

Pode parecer que escrever em idiomas com digitação dinâmica seja mais conveniente e mais simples do que em idiomas com digitação estática; no entanto, esses idiomas não foram criados por capricho de alguém. Durante seu desenvolvimento, os recursos dos sistemas de computadores foram levados em consideração. Tudo o que está escrito no texto do programa, no final, se resume às instruções do processador. Isso significa que os dados usados ​​pelo programa, por exemplo, na forma de objetos ou outros tipos de dados, também são convertidos em estruturas de baixo nível.

Python realiza essas transformações automaticamente, o programador não vê esses processos e não precisa cuidar dessas transformações.

Não ter que especificar o tipo de uma variável ao declarar que não é um recurso da linguagem que torna o Python lento. A arquitetura da linguagem torna possível tornar quase tudo dinâmico. Por exemplo, no tempo de execução, você pode substituir os métodos de objeto. Novamente, durante a execução do programa, você pode usar a técnica “monkey patch” aplicada a chamadas de sistema de baixo nível. No Python, quase tudo é possível.

É a arquitetura Python que torna a otimização extremamente difícil.

Para ilustrar essa idéia, vou usar uma ferramenta para rastrear chamadas de sistema no MacOS chamada DTrace.

Não há mecanismos de suporte ao DTrace na distribuição final do CPython, portanto, o CPython precisará ser recompilado com as configurações apropriadas. Aqui a versão 3.6.6 é usada. Então, usamos a seguinte sequência de ações:

 wget https://github.com/python/cpython/archive/v3.6.6.zip unzip v3.6.6.zip cd v3.6.6 ./configure --with-dtrace make 

Agora, usando python.exe , você pode usar o DTRace para rastrear o código. Leia sobre o uso do DTrace com Python aqui . E aqui você pode encontrar scripts para medir vários indicadores de desempenho de programas Python usando o DTrace. Entre eles estão os parâmetros para chamar funções, tempo de execução dos programas, tempo de uso do processador, informações sobre chamadas do sistema e assim por diante. Aqui está como usar o comando dtrace :

 sudo dtrace -s toolkit/<tracer>.d -c '../cpython/python.exe script.py' 

E aqui está como o py_callflow rastreamento py_callflow mostra as chamadas de função no aplicativo.


Rastreando usando o DTrace

Agora vamos responder à pergunta se a digitação dinâmica afeta o desempenho do Python. Aqui estão alguns pensamentos sobre isso:

  • A verificação e conversão de tipos são operações pesadas. Cada vez que uma variável é acessada, lida ou gravada, uma verificação de tipo é realizada.
  • É difícil otimizar uma linguagem com essa flexibilidade. A razão pela qual outras linguagens são muito mais rápidas que o Python é que elas comprometem escolhendo entre flexibilidade e desempenho.
  • O projeto Cython combina Python e tipagem estática, o que, por exemplo, como mostrado neste artigo , leva a melhorias de desempenho em 84 vezes em relação ao Python comum. Confira este projeto se precisar de velocidade.

Sumário


A razão para o fraco desempenho do Python é sua natureza dinâmica e versatilidade. Pode ser usado como uma ferramenta para resolver uma variedade de tarefas. Para atingir os mesmos objetivos, você pode tentar procurar ferramentas mais produtivas e melhor otimizadas. Talvez eles sejam capazes de encontrar, talvez não.

Os aplicativos escritos em Python podem ser otimizados usando os recursos de execução assíncrona de código, ferramentas de criação de perfil e - escolhendo o intérprete certo. Portanto, para otimizar a velocidade dos aplicativos cujo tempo de inicialização não é importante e cujo desempenho pode se beneficiar do uso do compilador JIT, considere usar o PyPy. Se você precisa de desempenho máximo e está pronto para as limitações da digitação estática, dê uma olhada no Cython.

Caros leitores! Como você resolve problemas ruins de desempenho do Python?

Source: https://habr.com/ru/post/pt418823/


All Articles