Existem limitações técnicas ou recursos de linguagem que impedem que meu script Python seja tão rápido quanto um programa C ++ equivalente?


10

Eu sou um usuário de Python de longa data. Alguns anos atrás, comecei a aprender C ++ para ver o que ele poderia oferecer em termos de velocidade. Durante esse período, eu continuaria usando o Python como uma ferramenta para prototipagem. Parecia que este era um bom sistema: desenvolvimento ágil com Python, execução rápida em C ++.

Recentemente, tenho usado o Python cada vez mais, e aprendendo a evitar todas as armadilhas e antipadrões que rapidamente usei nos meus anos anteriores com a linguagem. Entendo que o uso de certos recursos (lista de compreensões, enumerações etc.) pode aumentar o desempenho.

Mas existem limitações técnicas ou recursos de linguagem que impedem que meu script Python seja tão rápido quanto um programa C ++ equivalente?


2
Sim pode. Veja PyPy para o estado da arte nos compiladores Python.
Greg Hewgill 29/07

5
Todas as variáveis ​​em python são polimórficas, o que significa que o tipo da variável é conhecido apenas em tempo de execução. Se você vir (assumindo números inteiros) x + y em idiomas do tipo C, eles adicionam um número inteiro. Em python, haverá uma mudança nos tipos de variáveis ​​em xey, e então a função de adição apropriada será selecionada e, em seguida, haverá uma verificação de estouro e, em seguida, haverá a adição. A menos que o python aprenda a digitar estática, essa sobrecarga nunca desaparecerá.
nwp 29/07

1
@nwp Não, é fácil, veja PyPy. Os problemas mais complicados, ainda abertos, incluem: Como superar a latência de inicialização dos compiladores JIT, como evitar alocações para gráficos complicados de objetos de longa duração e como fazer bom uso do cache em geral.

Respostas:


11

Eu meio que bati nessa parede quando aceitei um trabalho de programação em Python em tempo integral há alguns anos. Eu amo Python, realmente, mas quando comecei a fazer alguns ajustes de desempenho, tive alguns choques rudes.

Os Pythonistas rigorosos podem me corrigir, mas aqui estão as coisas que encontrei, pintadas em traços muito amplos.

  • O uso de memória Python é meio assustador. Python representa tudo como um ditado - o que é extremamente poderoso, mas resulta que mesmo tipos de dados simples são gigantescos. Lembro que o caractere "a" ocupou 28 bytes de memória. Se você estiver usando estruturas de big data no Python, certifique-se de confiar em numpy ou scipy, porque elas são apoiadas pela implementação direta da matriz de bytes.

Isso tem um impacto no desempenho, porque significa que há níveis extras de indireção no tempo de execução, além de consumir enormes quantidades de memória em comparação com outros idiomas.

  • O Python possui um bloqueio de intérprete global, o que significa que, na maior parte dos casos, os processos estão executando o thread único. Pode haver bibliotecas que distribuem tarefas entre processos, mas estávamos rodando 32 instâncias do nosso script python e executando cada thread.

Outros podem conversar com o modelo de execução, mas o Python é um compilador em tempo de execução e, em seguida, é interpretado, o que significa que ele não percorre todo o caminho do código da máquina. Isso também tem um impacto no desempenho. Você pode facilmente vincular módulos C ou C ++ ou encontrá-los, mas se você executar o Python diretamente, terá um impacto no desempenho.

Agora, nos benchmarks de serviços da Web, o Python se compara favoravelmente com outras linguagens de compilação em tempo de execução, como Ruby ou PHP. Mas está bem atrás da maioria das linguagens compiladas. Até as linguagens que compilam na linguagem intermediária e executam em uma VM (como Java ou C #) fazem muito, muito melhor.

Aqui está um conjunto realmente interessante de testes de benchmark aos quais me refiro ocasionalmente:

http://www.techempower.com/benchmarks/

(Tudo o que foi dito, eu ainda amo muito o Python, e se tiver a chance de escolher o idioma em que estou trabalhando, é a minha primeira escolha. Na maioria das vezes, de qualquer forma, não sou limitado por requisitos de taxa de transferência loucos.)


2
A cadeia "a" não é um bom exemplo para o primeiro marcador. Uma cadeia Java também possui uma sobrecarga considerável para cadeias de caracteres únicas, mas é uma sobrecarga constante que se amortiza muito bem à medida que a cadeia cresce em comprimento (um a quatro bytes de caracteres, dependendo da versão, opções de compilação e conteúdo da cadeia). Você está certo sobre objetos definidos pelo usuário, pelo menos aqueles que não usam __slots__. PyPy deve se sair muito melhor nesse aspecto, mas não sei o suficiente para julgar.

1
O segundo problema que você está apontando está relacionado apenas à implementação específica e não é inerente ao idioma. O primeiro problema requer explicação: o que "pesa" 28 bytes não é o próprio personagem, mas o fato de ele ter sido empacotado em uma classe de string, acompanhando seus próprios métodos e propriedades. Representar um caractere como matriz de bytes (literal b'a ') "only" pesa 18 bytes no Python 3.3 e tenho certeza de que existem mais maneiras de otimizar o armazenamento de caracteres na memória, se o seu aplicativo realmente precisar.
Vermelho

O C # pode compilar de forma nativa (por exemplo, tecnologia MS futura, Xamarin para iOS).
Den

13

A implementação de referência do Python é o intérprete “CPython”. Ele tenta ser razoavelmente rápido, mas atualmente não emprega otimizações avançadas. E para muitos cenários de uso, isso é uma coisa boa: a compilação para algum código intermediário ocorre imediatamente antes do tempo de execução, e toda vez que o programa é executado, o código é compilado novamente. Portanto, o tempo necessário para a otimização deve ser ponderado em relação ao tempo ganho pelas otimizações - se não houver um ganho líquido, a otimização será inútil. Para um programa de execução muito longa, ou um programa com loops muito apertados, empregar otimizações avançadas seria útil. No entanto, o CPython é usado para alguns trabalhos que impedem a otimização agressiva:

  • Scripts de execução curta, usados, por exemplo, para tarefas sysadmin. Muitos sistemas operacionais como o Ubuntu constroem boa parte de sua infraestrutura em cima do Python: o CPython é rápido o suficiente para o trabalho, mas praticamente não tem tempo de inicialização. Contanto que seja mais rápido que o bash, é bom.

  • O CPython deve ter semântica clara, pois é uma implementação de referência. Isso permite otimizações simples como "otimizar a implementação do operador foo" ou "compilar as compreensões da lista para um bytecode mais rápido", mas geralmente impedirá otimizações que destroem informações, como funções embutidas.

Obviamente, existem mais implementações em Python do que apenas o CPython:

  • O Jython é construído sobre a JVM. A JVM pode interpretar ou compilar JIT o bytecode fornecido e possui otimizações guiadas por perfil. Ele sofre com um alto tempo de inicialização e leva um tempo até o JIT entrar em ação.

  • PyPy é um estado da arte, JITting Python VM. PyPy é escrito em RPython, um subconjunto restrito de Python. Esse subconjunto remove alguma expressividade do Python, mas permite que o tipo de qualquer variável seja inferido estaticamente. A VM gravada no RPython pode ser transpilada para C, o que fornece desempenho semelhante ao RPython C. No entanto, o RPython ainda é mais expressivo que o C, o que permite o desenvolvimento mais rápido de novas otimizações. PyPy é um exemplo de inicialização do compilador. O PyPy (não o RPython!) É compatível principalmente com a implementação de referência do CPython.

  • Cython é (como RPython) um dialeto incompatível com digitação estática. Ele também transpila para o código C e é capaz de gerar facilmente extensões C para o interpretador CPython.

Se você estiver disposto a traduzir seu código Python para Cython ou RPython, terá um desempenho semelhante ao C. No entanto, eles não devem ser entendidos como "um subconjunto do Python", mas como "C com sintaxe Pythonic". Se você mudar para o PyPy, seu código Python baunilha receberá um considerável aumento de velocidade, mas também não poderá interagir com extensões escritas em C ou C ++.

Mas quais propriedades ou recursos impedem o Python de baunilha de atingir níveis de desempenho do tipo C, além dos longos tempos de inicialização?

  • Contribuintes e financiamento. Ao contrário de Java ou C #, não há uma única empresa motriz por trás do idioma com interesse em fazer desse idioma o melhor de sua classe. Isso restringe o desenvolvimento principalmente a voluntários e concessões ocasionais.

  • Ligação tardia e falta de digitação estática. Python nos permite escrever porcaria assim:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding
    

    No Python, qualquer variável pode ser reatribuída a qualquer momento. Isso evita o armazenamento em cache ou inlining; qualquer acesso precisa passar pela variável. Esse indireção diminui o desempenho. Obviamente: se o seu código não faz coisas tão insanas para que cada variável possa receber um tipo definitivo antes da compilação e cada variável seja atribuída apenas uma vez, então - em teoria - um modelo de execução mais eficiente pode ser escolhido. Um idioma com isso em mente forneceria uma maneira de marcar identificadores como constantes e, pelo menos, permitir anotações de tipo opcionais ("digitação gradual").

  • Um modelo de objeto questionável. A menos que os slots sejam usados, é difícil descobrir quais campos um objeto possui (um objeto Python é essencialmente uma tabela de campos de hash). E mesmo quando estamos lá, ainda não temos idéia de quais tipos esses campos têm. Isso evita a representação de objetos como estruturas compactadas, como é o caso do C ++. (É claro que a representação de objetos em C ++ também não é ideal: devido à natureza estrutural, até os campos privados pertencem à interface pública de um objeto.)

  • Coleta de lixo. Em muitos casos, o GC poderia ser completamente evitado. C ++ nos permite alocar estaticamente objetos que são destruídos automaticamente quando o escopo atual é deixado: Type instance(args);. Até então, o objeto está vivo e pode ser emprestado para outras funções. Isso geralmente é feito via "passagem por referência". Idiomas como Rust permitem que o compilador verifique estaticamente se nenhum ponteiro para esse objeto excede a vida útil do objeto. Esse esquema de gerenciamento de memória é totalmente previsível, altamente eficiente e atende à maioria dos casos sem gráficos complicados de objetos. Infelizmente, o Python não foi projetado com o gerenciamento de memória em mente. Em teoria, a análise de escape pode ser usada para encontrar casos em que o GC pode ser evitado. Na prática, cadeias simples de métodos comofoo().bar().baz() terá que alocar um grande número de objetos de vida curta no heap (o GC geracional é uma maneira de manter esse problema pequeno).

    Em outros casos, o programador já pode saber o tamanho final de algum objeto, como uma lista. Infelizmente, o Python não oferece uma maneira de comunicar isso ao criar uma nova lista. Em vez disso, novos itens serão enviados para o final, o que pode exigir várias realocações. Algumas notas:

    • Listas de um tamanho específico podem ser criadas como fixed_size = [None] * size. No entanto, a memória dos objetos nessa lista precisará ser alocada separadamente. Contraste C ++, onde podemos fazer std::array<Type, size> fixed_size.

    • Matrizes compactadas de um tipo nativo específico podem ser criadas em Python através do arraymódulo interno. Além disso, numpyoferece representações eficientes de buffers de dados com formas específicas para tipos numéricos nativos.

Sumário

O Python foi projetado para facilitar o uso, não para o desempenho. Seu design dificulta a criação de uma implementação altamente eficiente. Se o programador se abster de recursos problemáticos, um compilador que entenda os idiomas restantes poderá emitir código eficiente que pode rivalizar com C no desempenho.


8

Sim. O principal problema é que a linguagem é definida como dinâmica - ou seja, você nunca sabe o que está fazendo até estar prestes a fazê-lo. Isso faz com que seja muito difícil de produzir código máquina eficiente, porque você não sabe o que o código de máquina produtos para . Os compiladores JIT podem fazer algum trabalho nessa área, mas nunca é comparável ao C ++, porque o compilador JIT simplesmente não pode gastar tempo e memória executando, pois esse é o tempo e a memória que você não está gastando executando seu programa, e há limites rígidos sobre o que eles podem alcançar sem quebrar a semântica da linguagem dinâmica.

Não vou afirmar que essa é uma troca inaceitável. Mas é fundamental para a natureza do Python que implementações reais nunca sejam tão rápidas quanto as implementações em C ++.


8

Existem três fatores principais que afetam o desempenho de todas as linguagens dinâmicas, algumas mais que outras.

  1. Sobrecarga interpretativa. No tempo de execução, existe algum tipo de código de bytes, em vez de instruções da máquina, e há uma sobrecarga fixa na execução desse código.
  2. Despesas gerais de expedição. O destino de uma chamada de função não é conhecido até o tempo de execução, e descobrir qual método chamar implica um custo.
  3. Sobrecarga de gerenciamento de memória. Linguagens dinâmicas armazenam coisas em objetos que precisam ser alocados e desalocados, e que carregam sobrecarga de desempenho.

Para C / C ++, os custos relativos desses três fatores são quase zero. As instruções são executadas diretamente pelo processador, o despacho leva no máximo um ou dois indícios, a memória heap nunca é alocada, a menos que você o diga. Código bem escrito pode se aproximar da linguagem assembly.

Para C # / Java com compilação JIT, os dois primeiros são baixos, mas a memória coletada de lixo tem um custo. Um código bem escrito pode se aproximar de 2x C / C ++.

Para Python / Ruby / Perl, o custo de todos esses três fatores é relativamente alto. Pense 5x em comparação com C / C ++ ou pior. (*)

Lembre-se de que o código da biblioteca de tempo de execução pode muito bem ser escrito no mesmo idioma dos seus programas e ter as mesmas limitações de desempenho.


(*) À medida que a compilação Just-In_Time (JIT) é estendida a esses idiomas, eles também se aproximam (normalmente 2x) da velocidade do código C / C ++ bem escrito.

Deve-se notar também que, uma vez que a lacuna é estreita (entre idiomas concorrentes), as diferenças são dominadas por algoritmos e detalhes de implementação. O código JIT pode superar o C / C ++ e o C / C ++ pode superar a linguagem assembly, porque é apenas mais fácil escrever um bom código.


"Lembre-se de que o código da biblioteca de tempo de execução pode ser escrito no mesmo idioma dos seus programas e ter as mesmas limitações de desempenho." e "Para Python / Ruby / Perl, o custo de todos esses três fatores é relativamente alto. Pense 5x em comparação com C / C ++ ou pior." Atualmente isso não é verdade. Por exemplo, a Hashclasse Rubinius (uma das principais estruturas de dados em Ruby) é escrita em Ruby e apresenta um desempenho comparável, às vezes até mais rápido, do que a Hashclasse de YARV, escrita em C. E uma das razões é que grandes partes do tempo de execução de Rubinius sistema são escritas em ruby, de modo que eles podem ...
Jörg W Mittag

… Por exemplo, seja delineado pelo compilador Rubinius. Exemplos extremos são a Klein VM (uma VM metacircular para Self) e a Maxine VM (uma VM metacircular para Java), onde tudo , até o código de despacho do método, coletor de lixo, alocador de memória, tipos primitivos, estruturas de dados e algoritmos principais, são escritos em Próprio ou Java. Dessa forma, mesmo partes da VM principal podem ser incorporadas no código de usuário, e a VM pode se recompilar e otimizar novamente usando feedback de tempo de execução do programa do usuário.
Jörg W Mittag

@ JörgWMittag: Ainda é verdade. Rubinius possui JIT, e o código JIT geralmente supera o C / C ++ em benchmarks individuais. Não consigo encontrar nenhuma evidência de que esse material metacircular faça muito pela velocidade na ausência de JIT. [Veja a edição para maior clareza sobre o JIT.]
david.pfx 18/08/14

1

Mas existem limitações técnicas ou recursos de linguagem que impedem que meu script Python seja tão rápido quanto um programa C ++ equivalente?

Não. É apenas uma questão de dinheiro e recursos despejados para fazer o C ++ rodar rápido versus dinheiro e recursos despejados para fazer o Python rodar rápido.

Por exemplo, quando a Self VM foi lançada, não era apenas o idioma OO dinâmico mais rápido, era o período do idioma OO mais rápido. Apesar de ser uma linguagem incrivelmente dinâmica (muito mais que Python, Ruby, PHP ou JavaScript, por exemplo), foi mais rápida que a maioria das implementações de C ++ disponíveis.

Mas a Sun cancelou o projeto Self (uma linguagem OO de uso geral madura para o desenvolvimento de grandes sistemas) para se concentrar em uma pequena linguagem de script para menus animados em caixas de TV (você deve ter ouvido falar sobre isso, chama-se Java). mais financiamento. Ao mesmo tempo, Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. gastou grandes quantias de dinheiro e recursos, tornando o C ++ rápido. Os fabricantes de CPU adicionaram recursos aos seus chips para acelerar o C ++. Os sistemas operacionais foram escritos ou modificados para acelerar o C ++. Então, o C ++ é rápido.

Não estou muito familiarizado com o Python, sou mais uma pessoa Ruby, por isso darei um exemplo do Ruby: a Hashclasse (equivalente em função e importância ao dictPython) na implementação do Rubinius Ruby é escrita em 100% puro Ruby; no entanto, ele compete favoravelmente e às vezes supera a Hashclasse no YARV, escrita em C. otimizado à mão. E comparado a alguns dos sistemas comerciais Lisp ou Smalltalk (ou a mencionada Self VM), o compilador de Rubinius não é tão inteligente .

Não há nada inerente ao Python que o torne lento. Existem recursos nos processadores e sistemas operacionais de hoje que prejudicam o Python (por exemplo, a memória virtual é conhecida por ser terrível para o desempenho da coleta de lixo). Existem recursos que ajudam o C ++, mas não ajudam o Python (as CPUs modernas tentam evitar falhas de cache, porque são muito caras. Infelizmente, é difícil evitar falhas de cache quando você tem OO e polimorfismo. Em vez disso, você deve reduzir o custo do cache A CPU Azul Vega, projetada para Java, faz isso.)

Se você gasta tanto dinheiro, pesquisa e recursos para tornar o Python rápido, como foi feito para C ++, e gasta tanto dinheiro, pesquisa e recursos para tornar sistemas operacionais que fazem os programas em Python rodarem mais rápido quanto foi feito para C ++ e você gasta como muito dinheiro, pesquisa e recursos para criar CPUs que executam programas em Python mais rápido que o C ++, então não há dúvida de que o Python poderia atingir desempenho comparável ao C ++.

Vimos com o ECMAScript o que pode acontecer se apenas um jogador leva a sério o desempenho. Em um ano, tivemos basicamente um aumento de 10 vezes no desempenho geral para todos os principais fornecedores.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.