Quando eu era pequeno, a programação era simples. Meu amigo tinha um computador e havia Básico e Montagem. Você poderia escrever seu programa em Basic, que era mais fácil de fazer, mas seu programa seria lento, ou você poderia escrever algo em Assembly, que era mais difícil, mas seu programa rodaria significativamente mais rápido.
A explicação para isso também era simples. Basic era um interpretador, para executar seu programa, ele tinha que passar por seu código toda vez que você o invocasse e interpretá-lo linha por linha. Se diz "PRINT X", o interpretador deve encontrar uma variável chamada “X”, encontrar uma rotina que imprima e chamar a rotina encontrada para a variável encontrada.
Assembléia era, bem, assembléia. Ele também interpretou seu programa de certa forma, mas apenas uma vez quando você executa o montador. Depois disso, seu programa seria executável sem interpretação. Os programas que precisam de interpretação são executados mais lentamente do que os programas que não precisam de interpretação. Se, claro, forem programas equivalentes.
E em Básico e Montagem, eles geralmente eram. Basic é uma linguagem imperativa, nem mesmo muito amigável para programação estrutural. Mesmo algo básico como uma "função" não é uma construção de linguagem interna, mas um padrão: "GOSUB ... RETURN", muito parecido com "call ... ret" em Assembly.
Agora avance 30 anos. As línguas são abundantes. Os computadores estão por toda parte. A programação não é mais simples. Meu departamento ganha seu pão reescrevendo o código do pesquisador originalmente escrito em Python em C++ para desempenho porque o conhecimento comum é que Python é interpretado e lento, e C++ é compilado e rápido. Mas de alguma forma, a cada ano essa reescrita fica cada vez mais difícil de ganhar qualquer performance. Algo muda e muda rápido. O conhecimento comum, entretanto, não muda, então continuamos reescrevendo.
Mas agora somos forçados a otimizar tudo como loucos apenas para justificar o que fazemos. Um algoritmo entra em Python, nós o reescrevemos em C++ de forma equivalente e, de repente, ele se torna 3 vezes mais lento. Não é para isso que estamos aqui. Portanto, reprojetamos o algoritmo para impulsionar e obter o aumento de desempenho que prometemos. E na maioria das vezes, uma vez que os pesquisadores não se importam com o desempenho e, em termos algorítmicos, eles deixam alguns frutos fáceis para trás, isso funciona.
Ainda assim, todo esse negócio agora parece uma farsa. Estamos tornando o código mais lento reescrevendo-o em C++ apenas para que possamos torná-lo mais rápido com a reengenharia do código. Então, por que não reprojetamos diretamente em Python? Ah! O fato é que não conhecemos Python. Conhecemos um pouco de Python, o suficiente para ler e entender, mas não o suficiente para fazer programas ultrarrápidos nele.
Então, o que há para saber?
A maioria das bibliotecas Python são escritas em C ou Fortran. O núcleo NumPy é escrito em C; Pandas - em Cython e C; SciPy - em Fortran, C e parcialmente C++. Eles não têm motivos para serem mais lentos do que o que foi escrito em C++, Rust ou Julia. Eles podem ser mais rápidos embora.
Em nossa empresa, atendemos serviços em nuvem e aplicativos de desktop. E os usuários de desktop ficam irritados quando a nova versão de seu aplicativo favorito para de funcionar em seu hardware aparentemente sem motivo. Portanto, mantemos nosso destino de compilações de desktop antigo. Realmente velho, como o velho pré-Nehalem. Dessa forma, ninguém fica com raiva, mas também ninguém consegue aproveitar o SSE3.
Claro, uma biblioteca computacional construída para um alvo adequado será geralmente mais rápida do que uma biblioteca equivalente construída para um computador genérico de 15 anos com capacidades superescalares limitadas.
A boa notícia é que, se você estiver construindo para uma nuvem, poderá definir sua plataforma de compilação de destino para ser exatamente a máquina que você adquirir e, em seguida, suas bibliotecas C++ serão executadas na mesma velocidade que as do Python e talvez até um pouco mais rápido.
Para ser honesto, todo o argumento sobre qual linguagem é mais rápida é ridículo. Uma linguagem não é um compilador ou um interpretador, é o que ela é – uma linguagem: um conjunto de regras que especificam como devemos dizer a um computador o que queremos fazer. Uma linguagem é apenas um conjunto de regras, uma especificação. E nada mais.
A própria distinção entre interpretação e compilação é algo do século passado. Hoje em dia, existem interpretadores C como IGCC , PicoC ou CCons , e existem compiladores Python. Compiladores JIT, como [PyPy] e compiladores clássicos de compilação antes de executar, como Codon (que também possui capacidade JIT se você deseja que apenas parte do seu código seja compilado).
O Codon é construído sobre o LLVM, a mesma infraestrutura sobre a qual Rust, Julia ou Clang são construídos. O código criado com o Codon é executado, mais ou menos, nos mesmos níveis de desempenho criados com qualquer um deles. Pode haver desvantagens de desempenho devido à coleta de lixo do Python ou a grandes tipos de dados nativos, mas não estamos mais falando de 100x ou 10x. LLVM faz sua mágica. ele transforma o código Python em código de máquina para você.
Também existem mitos sobre a compilação just-in-time ou JIT. Alguns dizem que é superior à técnica usual de compilar antes de executar porque sempre compila para a arquitetura que os usuários têm e, portanto, a explora de maneira otimizada. Alguns dizem que ainda há sobrecarga de compilação que, com a compilação just-in-time, também recai sobre os usuários. Isso faz com que os programas sejam executados lentamente, pois precisam executar e compilar-se ao mesmo tempo.
O problema com ambos os mitos é que ambos são verdadeiros e inúteis. Sim, o JIT geralmente compila para um código de máquina melhor, a menos que você crie seus binários explicitamente para a máquina de destino, o que, aliás, acontece com bastante regularidade quando você implanta em uma nuvem. E sim, há uma penalidade de compilação em tempo de execução, mas é insignificante se seu tempo de execução for medido em meses, o que, novamente, quando você implanta em uma nuvem, não é algo inédito.
Portanto, há prós e contras. O que é importante, o Python (especificamente o Codon) oferece suporte aos modos de compilação antes de executar e JIT, para que você possa escolher o que mais atende às suas necessidades. Os compiladores tradicionais, como o Clang, não possuem a opção JIT.
Falando em JIT, Numba é provavelmente a tecnologia que mais muda o jogo no mundo da programação Python ultrarrápida. É um compilador, mas visa apenas kernels selecionados, não o programa inteiro. Você, é claro, pode selecionar o que deve ser compilado e para qual plataforma. Nesta configuração, você pode executar partes do seu código na CPU e outras - na GPGPU .
Tecnicamente, pode-se criar back-ends para outros dispositivos especializados, como o TPU do Google ou mesmo o acelerador fotônico da Lightmatters. Ainda não existe esse back-end, esses caras decidiram lançar sua própria biblioteca. Mas, o que é sintomático, eles também optam por fornecer a interface para o computador fotônico em Python para que você possa interagir com Pytorch, Tensorflow ou ONNX sem problemas.
Então Lightmatter ainda não está lá. Mas a NVidia é. Eles forneceram seu back-end CUDA para Numba e agora você pode escrever kernels em Python e executá-los em hardware NVidia com eficiência máxima. C++ não tem isso. Existe, no entanto, um dialeto CU, vindo da NVidia , é claro, que estende o C++ para esse assunto. Em Python, você não precisa estender a própria linguagem. Como o Numba funciona como um compilador de kernel JIT, adicionar um back-end é apenas uma questão de corrigir uma biblioteca.
Portanto, o modelo do kernel visa a computação heterogênea. Você pode executar trechos de código em diferentes dispositivos, o que é bom por si só. Mas há outra dimensão de heterogeneidade na qual você pode não ter pensado. Com o modelo de kernel, você pode direcionar diferentes kernels para diferentes contextos de computação e não necessariamente para dispositivos de hardware. Isso significa que, se você deseja que um kernel seja rápido, mas não particularmente preciso, pode construí-lo com a opção “-fast-math”. Mas se, em algum outro contexto, você quiser que o kernel seja preciso em vez de rápido, você pode reconstruir o mesmo código sem a compensação.
Isso é algo difícil de conseguir com compiladores tradicionais, onde você não pode alterar as opções de compilação no meio de uma unidade de tradução. Bem, com o modelo do kernel, cada kernel é sua própria unidade de tradução.
Python não é lento. Nem é rápido. É apenas uma linguagem, um conjunto de regras e palavras-chave. Mas tem muita gente que se acostumou com essas regras e essas palavras-chave. Eles se sentem confortáveis escrevendo em Python e estão interessados em tornar o Python melhor para eles.
Essa base de usuários é grande o suficiente para atrair novas startups com tecnologias revolucionárias, como Lightmatter e seus computadores fotônicos, e empresas bem estabelecidas com décadas de experiência em computação de alto desempenho, como a NVidia. Todas essas pessoas estão muito empenhadas em tornar o Python uma linguagem melhor... não uma linguagem, é claro, mas um ambiente. O ambiente em que escrever programas ultra-rápidos é apenas um pouco mais difícil do que escrever programas lentos descartáveis.
Juntos, eles estão fazendo grandes progressos. Python está ficando mais rápido a cada ano. Neste ponto, esqueça os computadores fotônicos, se puder, os programas escritos em Python geralmente são executados no mesmo nível daqueles escritos em Julia, C++ ou Rust. Mas o Python não pararia por aí. Está ficando mais rápido do que os compiladores tradicionais estão ficando mais amigáveis.
Esteja preparado para ver que em alguns anos os compiladores Python: PyPy, Numba ou algo completamente novo, emprestarão técnicas de Spiral ou Herbie para gerar código com muito mais eficiência que nenhum compilador tradicional poderia chegar perto. Afinal, escrever um novo back-end JIT em Python é muito mais fácil do que reimaginar toda a infraestrutura LLVM.