Uma linguagem dinâmica como Ruby / Python pode alcançar desempenho semelhante ao C / C ++?


64

Gostaria de saber se é possível construir compiladores para linguagens dinâmicas como Ruby para ter desempenho semelhante e comparável ao C / C ++? Pelo que entendi sobre compiladores, como o Ruby, por exemplo, compilar código Ruby nunca pode ser eficiente, porque a maneira como o Ruby lida com reflexão, recursos como a conversão automática de tipo de número inteiro para número inteiro grande e a falta de digitação estática tornam a construção de um compilador eficiente para Ruby extremamente difícil.

É possível criar um compilador que possa compilar Ruby ou qualquer outra linguagem dinâmica em um binário com desempenho muito próximo ao C / C ++? Existe uma razão fundamental para os compiladores JIT, como PyPy / Rubinius, eventualmente ou nunca corresponderem a C / C ++ no desempenho?

Nota: Entendo que o “desempenho” pode ser vago; portanto, para esclarecer isso, eu quis dizer, se você pode fazer X em C / C ++ com desempenho Y, você pode fazer X em Ruby / Python com desempenho próximo a Y? Onde X é tudo, desde drivers de dispositivo e código do SO até aplicativos da web.


11
Você pode reformular a pergunta para que ela encoraje respostas apoiadas em evidências adequadas sobre outras pessoas?
Raphael

@ Rafael Eu fui em frente e editei. Acho que minha edição não altera fundamentalmente o significado da pergunta, mas a torna menos convidativa para artigos de opinião.
Gilles 'SO- stop be evil' em

11
Em particular, acho que você precisa corrigir uma (ou algumas) medidas concretas de desempenho. Tempo de execução? Uso de espaço? Uso de energia? Hora do desenvolvedor? Retorno de investimento? Observe também esta pergunta em nossa meta que diz respeito a essa pergunta (ou melhor, a suas respostas).
Raphael

Esta questão é um iniciador típico de guerras religiosas. E como podemos ver pelas respostas, estamos tendo uma, embora muito civilizada.
Andrej Bauer

Existem idiomas dinâmicos que permitem anotações de tipo opcionais (por exemplo: Clojure). Pelo que sei, o desempenho associado às funções anotadas por tipo é equivalente a quando um idioma seria digitado estaticamente.
Pedro Morte Rolo

Respostas:


68

Para todos aqueles que disseram "sim", vou oferecer um contraponto de que a resposta é "não", por padrão . Esses idiomas nunca poderão corresponder ao desempenho de idiomas compilados estaticamente.

Kos ofereceu o argumento (muito válido) de que as linguagens dinâmicas têm mais informações sobre o sistema em tempo de execução, que podem ser usadas para otimizar o código.

No entanto, existe outro lado da moeda: essas informações adicionais precisam ser mantidas em sigilo. Nas arquiteturas modernas, isso é um fator que mata o desempenho.

William Edwards oferece uma boa visão geral do argumento .

Em particular, as otimizações mencionadas por Kos não podem ser aplicadas além de um escopo muito limitado, a menos que você limite o poder expressivo de seus idiomas de maneira bastante drástica, como mencionado por Devin. É claro que isso é uma troca viável, mas, para o bem da discussão, você acaba com uma linguagem estática , não dinâmica. Essas linguagens diferem fundamentalmente de Python ou Ruby, como a maioria das pessoas as entenderia.

William cita alguns slides interessantes da IBM :

  • Cada variável pode ser digitada dinamicamente: Precisa de verificações de tipo
  • Toda instrução pode gerar exceções devido a incompatibilidade de tipo e assim por diante: Precisa de verificações de exceção
  • Cada campo e símbolo pode ser adicionado, excluído e alterado em tempo de execução: Precisa de verificações de acesso
  • O tipo de cada objeto e sua hierarquia de classes pode ser alterado em tempo de execução: Precisa de verificações de hierarquia de classes

Algumas dessas verificações podem ser eliminadas após a análise (Nota: essa análise também leva tempo - em tempo de execução).

Além disso, Kos argumenta que as linguagens dinâmicas podem até superar o desempenho do C ++. O JIT pode realmente analisar o comportamento do programa e aplicar otimizações adequadas.

Mas os compiladores C ++ podem fazer o mesmo! Os compiladores modernos oferecem a chamada otimização guiada por perfil que, se receberem uma entrada adequada, pode modelar o comportamento do tempo de execução do programa e aplicar as mesmas otimizações que um JIT aplicaria.

Obviamente, tudo isso depende da existência de dados de treinamento realistas e, além disso, o programa não pode adaptar suas características de tempo de execução se o padrão de uso mudar no meio da execução. JITs podem teoricamente lidar com isso. Eu ficaria interessado em ver como isso se sai na prática, pois, para alternar otimizações, o JIT precisaria coletar continuamente dados de uso que mais uma vez atrasam a execução.

Em resumo, não estou convencido de que as otimizações de hot spot de tempo de execução superem a sobrecarga de rastrear informações de tempo de execução a longo prazo , em comparação com a análise e otimização estáticas.


2
@Raphael Isso é uma “falha” do compilador então. Em particular, javacalguma vez a otimização guiada por perfil? Não tanto quanto eu sei. Em geral, não faz sentido otimizar o compilador de uma linguagem JIT, uma vez que a JIT pode lidar com ela (e, no mínimo, desta forma, mais linguagens lucram com o esforço). Então (compreensivelmente) nunca houve muito esforço no javacotimizador, tanto quanto eu sei (para as linguagens .NET isso é definitivamente verdade).
Konrad Rudolph

11
@ Rafael A palavra-chave é: talvez. Não mostra de qualquer maneira. Isso é tudo que eu queria dizer. Eu dei razões (mas nenhuma prova) para minha suposição nos parágrafos anteriores.
27912 Konrad Rudolph

11
@ Ben Eu não nego que é complicado. Isso é apenas uma intuição. Rastrear todas essas informações em tempo de execução tem um custo, afinal. Não estou convencido de seu ponto de vista sobre IO. Se isso for previsível (= caso de uso típico), o PGO poderá prever. Se for falso, não estou convencido de que o JIT também possa otimizá-lo. Talvez de vez em quando, por pura sorte. Mas de forma confiável? ...
Konrad Rudolph

2
@ Konrad: Sua intuição é falsa. Não se trata de variar em tempo de execução, é de imprevisibilidade em tempo de compilação . O ponto ideal para JITs versus otimização estática não é quando o comportamento do programa muda em tempo de execução "muito rápido" para criação de perfil, é quando o comportamento do programa é fácil de otimizar em cada execução individual do programa, mas varia muito entre corre. Um otimizador estático geralmente precisará otimizar apenas um conjunto de condições, enquanto um JIT otimiza cada execução separadamente, para as condições que ocorrem nessa execução.
Ben

3
Aviso: este tópico de comentários está se transformando em uma mini sala de bate-papo. Se você quiser continuar esta discussão, leve-a para o bate-papo. Os comentários devem ser usados ​​para melhorar a postagem original. Interrompa esta conversa aqui. Obrigado.
Robert Cartaino

20

se você pode executar X em C / C ++ com desempenho Y, pode executar X em Ruby / Python com desempenho próximo a Y?

Sim. Tome, como exemplo, o PyPy. É uma coleção de código Python que executa perto de C ao interpretar (nem tanto assim, mas também não tão longe). Ele faz isso através da realização de análises em programa completo no código fonte para atribuir cada variável um tipo estático (ver a Annotator e Rtyper docs para mais detalhes), e então, uma vez armado com a mesma informação do tipo que você dá C, ele pode executar a mesma tipos de otimizações. Pelo menos em teoria.

A desvantagem, é claro, é que apenas um subconjunto de código Python é aceito pelo RPython e, em geral, mesmo que essa restrição seja levantada, apenas um subconjunto de código Python pode se sair bem: o subconjunto que pode ser analisado e receber tipos estáticos.

Se você restringir o Python o suficiente, poderão ser criados otimizadores que tirem proveito do subconjunto restrito e o compilem em código eficiente. Este não é realmente um benefício interessante, na verdade, é bem conhecido. Mas o ponto principal de usar Python (ou Ruby) em primeiro lugar era que queríamos usar recursos interessantes que talvez não analisem bem e resultem em bom desempenho! Então a pergunta interessante é realmente ...

Além disso, os compiladores JIT, como PyPy / Rubinius, serão compatíveis com C / C ++ em desempenho?

Nah.

Com o que quero dizer: claro, talvez à medida que o código seja acumulado, você possa obter informações de digitação e pontos de acesso suficientes para compilar todo o código até o código da máquina. E talvez possamos obter um desempenho melhor que C para algum código. Eu não acho isso extremamente controverso. Mas ainda precisa "aquecer", e o desempenho ainda é um pouco menos previsível, e não será tão bom quanto o C ou C ++ para determinadas tarefas que exigem um desempenho consistente e previsível.

Os dados de desempenho existentes para Java, que possuem mais informações de tipo que Python ou Ruby, e um compilador JIT melhor desenvolvido que Python ou Ruby, ainda não correspondem ao C / C ++. É, no entanto, no mesmo estádio.


11
"A desvantagem, é claro, é que apenas um subconjunto do código Python é aceito, ou melhor, apenas um subconjunto do código Python pode se sair bem: o subconjunto que pode ser analisado e receber tipos estáticos". Isso não é bem preciso. Somente o subconjunto pode ser aceito pelo compilador RPython. Compilar no RPython para um C razoavelmente eficiente só funciona exatamente porque as partes difíceis de compilar do Python garantem que nunca ocorram nos programas RPython; não é apenas que, se ocorrerem, não serão otimizados. É o intérprete compilado que lida com todo o Python.
Ben

11
Sobre o JIT: Vi mais de um benchmark em que o Java superou vários tipos de C (++). Somente C ++ com impulso parece permanecer à frente de forma confiável. Nesse caso, eu me pergunto sobre o desempenho por tempo de desenvolvedor, mas esse é outro tópico.
Raphael

@ Ben: depois que você tiver o RPython, é trivial criar um compilador / intérprete que volte a usar o interpretador CPython quando o compilador RPython falhar; portanto, "apenas um subconjunto do código Python pode fazer bem: ..." é totalmente preciso.
Lie Ryan

9
@ Raphael Já foi demonstrado muitas vezes que o código C ++ bem escrito supera o Java. É a parte "bem escrita" que é um pouco mais difícil de obter em C ++, portanto, em muitos bancos, você vê os resultados que o Java supera o C ++. O C ++ é, portanto, mais caro, mas quando o controle rígido da memória e a granulação de metal são necessários, C / C ++ é o que você procura. C em particular é apenas um montador de AC / P.
TC1

7
Comparar o desempenho máximo de outros idiomas com idiomas como C / C ++ é uma espécie de exercício de futilidade, pois é possível incorporar o assembly diretamente como parte da especificação do idioma. Qualquer coisa que a máquina possa fazer executando um programa escrito em qualquer idioma, você pode duplicar escrevendo o assembly a partir de um rastreamento de instruções executadas. Uma métrica muito mais interessante seria, como sugere @Raphael em um comentário anterior, desempenho por esforço de desenvolvimento (horas de trabalho, linhas de código etc.).
Patrick87

18

A resposta curta é: não sabemos , pergunte novamente em 100 anos. (Ainda podemos não saber então; possivelmente nunca saberemos.)

Em teoria, isso é possível. Pegue todos os programas já escritos, traduza-os manualmente para o código de máquina mais eficiente possível e escreva um intérprete que mapeie os códigos-fonte para os códigos de máquina. Isso é possível, pois apenas um número finito de programas já foi gravado (e, à medida que mais programas são gravados, mantenha as traduções manuais). Isso também é, obviamente, completamente idiota em termos práticos.

Por outro lado, teoricamente, linguagens de alto nível podem conseguir o desempenho do código de máquina, mas não o superam. Isso ainda é muito teórico, porque, em termos práticos, raramente recorremos à escrita de código de máquina. Esse argumento não se aplica à comparação de linguagens de nível superior: não implica que C deva ser mais eficiente que Python, apenas esse código de máquina não pode ser pior que Python.

Vindo do outro lado, em termos puramente experimentais, podemos ver que na maioria das vezes , as linguagens de alto nível interpretadas apresentam desempenho pior do que as linguagens de baixo nível compiladas. Tendemos a escrever código não sensível ao tempo em linguagens de alto nível e loops internos críticos no assembly, com linguagens como C e Python entrando no meio. Embora eu não tenha nenhuma estatística para apoiar isso, acho que essa é realmente a melhor decisão na maioria dos casos.

No entanto, existem instâncias incontestadas em que linguagens de alto nível superam o código que seria gravado realisticamente: ambientes de programação para fins especiais. Programas como Matlab e Mathematica costumam ser muito melhores na solução de certos tipos de problemas matemáticos do que o que meros mortais podem escrever. As funções da biblioteca podem ter sido escritas em C ou C ++ (que é um combustível para o campo “linguagens de baixo nível são mais eficientes”), mas isso não é da minha conta se estou escrevendo código do Mathematica, a biblioteca é uma caixa preta.

É teoricamente possível que o Python se aproxime, ou talvez até mais, do desempenho ideal que o C? Como visto acima, sim, mas estamos muito longe disso hoje. Por outro lado, os compiladores fizeram muito progresso nas últimas décadas, e esse progresso não está diminuindo.

Linguagens de alto nível tendem a automatizar mais as coisas, por isso têm mais trabalho a executar e, portanto, tendem a ser menos eficientes. Por outro lado, eles tendem a ter mais informações semânticas, portanto, pode ser mais fácil detectar otimizações (se você estiver escrevendo um compilador Haskell, não precisa se preocupar que outro segmento modifique uma variável sob o seu nariz). Um dos vários esforços para comparar diferentes linguagens de programação de maçãs e laranjas é o Computer Benchmark Game (anteriormente conhecido como shootout). Fortran tende a brilhar em tarefas numéricas; mas quando se trata de manipular dados estruturados ou comutação de encadeamento de alta taxa, F # e Scala se saem bem. Não tome esses resultados como evangelho: muito do que eles estão medindo é quão bom o autor do programa de testes em cada idioma foi.

Um argumento a favor de linguagens de alto nível é que o desempenho em sistemas modernos não está tão fortemente correlacionado com o número de instruções executadas, e menos com o tempo. Linguagens de baixo nível são boas correspondências para máquinas sequenciais simples. Se um idioma de alto nível executar duas vezes mais instruções, mas conseguir usar o cache de maneira mais inteligente, de maneira que ele faça a metade do número de erros de cache, poderá ser o vencedor.

Nas plataformas de servidor e desktop, as CPUs quase atingiram um platô em que não ficam mais rápidas (as plataformas móveis também estão chegando lá); isso favorece linguagens em que o paralelismo é fácil de explorar. Muitos processadores passam a maior parte do tempo aguardando uma resposta de E / S; o tempo gasto em computação é pouco comparado com a quantidade de E / S, e uma linguagem que permite ao programador minimizar as comunicações é uma vantagem.

Em suma, enquanto os idiomas de alto nível começam com uma penalidade, eles têm mais espaço para melhorias. Quão perto eles podem chegar? Pergunte novamente em 100 anos.

Nota final: frequentemente, a comparação não é entre o programa mais eficiente que pode ser escrito na língua A e o mesmo na linguagem B, nem entre o programa mais eficiente já escrito em cada idioma, mas entre o programa mais eficiente que pode ser escrito por um ser humano em um determinado período de tempo em cada idioma. Isso introduz um elemento que não pode ser analisado matematicamente, mesmo em princípio. Em termos práticos, isso geralmente significa que o melhor desempenho é um compromisso entre quanto código de baixo nível você precisa escrever para atingir as metas de desempenho e quanto código de baixo nível você tem tempo para escrever para cumprir as datas de lançamento.


Eu acho que a distinção entre alto e baixo nível é a errada. C ++ pode ser (muito) alto nível. No entanto, o C ++ moderno e de alto nível não (necessariamente) apresenta desempenho pior do que um equivalente de baixo nível - muito pelo contrário. O C ++ e suas bibliotecas foram cuidadosamente projetados para oferecer abstrações de alto nível sem penalidades de desempenho. O mesmo vale para o seu exemplo Haskell: suas abstrações de alto nível geralmente permitem, em vez de impedir otimizações. A distinção original entre linguagens dinâmicas e linguagens estáticas faz mais sentido nesse sentido.
Konrad Rudolph

@KonradRudolph Você está certo, pois nível baixo / alto é uma distinção um tanto arbitrária. Mas linguagens dinâmicas versus estáticas também não capturam tudo; um JIT pode eliminar grande parte da diferença. Essencialmente, as respostas teóricas conhecidas a essa pergunta são triviais e inúteis, e a resposta prática é "depende".
Gilles 'SO- stop be evil'

Bem, então, acho que a pergunta se torna "quão bom os JITs se tornam e, se eles ultrapassam a compilação estática, as linguagens estaticamente compiladas também podem lucrar com eles?" E sim, concordo com a sua avaliação, mas certamente podemos obter alguns palpites informados que vão além de "depende". ;-)
Konrad Rudolph

@KonradRudolph Se eu quisesse adivinhar, eu perguntaria sobre Engenharia de Software .
Gilles 'SO- stop be evil'

11
Infelizmente, o tiroteio do idioma é uma fonte questionável para benchmarks quantitativos: eles não aceitam todos os programas apenas aqueles considerados típicos do idioma. Este é um requisito complicado e muito subjetivo; significa que você não pode supor que uma implementação de tiroteio seja realmente boa (e, na prática, algumas implementações têm obviamente alternativas superiores rejeitadas). Por outro lado; essas são marcas de microbench e algumas implementações usam técnicas incomuns que você nunca consideraria em um cenário mais normal apenas para obter desempenho. Então: é um jogo, não uma fonte de dados muito confiável.
Eamon Nerbonne

10

A diferença básica entre a declaração C ++ x = a + be a declaração Python x = a + bé que um compilador C / C ++ pode dizer a partir desta declaração (e um pouco de informação extra que ela tem prontamente disponíveis sobre os tipos de x, ae b) precisamente o código de máquina precisa ser executado . Considerando que, para dizer quais operações a instrução Python vai fazer, é necessário resolver o Problema da Parada.

Em C, essa instrução basicamente será compilada para um dos poucos tipos de adição de máquina (e o compilador C sabe qual). No C ++, ele pode ser compilado dessa maneira, ou pode ser chamado para chamar uma função estaticamente conhecida ou (no pior caso) pode ser necessário compilar para uma consulta e chamada de método virtual, mas mesmo isso tem uma sobrecarga de código de máquina bastante pequena. Mais importante, porém, o compilador C ++ pode dizer dos tipos estaticamente envolvidos envolvidos se pode emitir uma única operação de adição rápida ou se precisa usar uma das opções mais lentas.

Em Python, teoricamente, um compilador poderia fazer quase esse bem se soubesse disso ae bfosse ambos int. Há alguma sobrecarga adicional de boxe, mas se os tipos forem estaticamente conhecidos, você provavelmente poderá se livrar disso também (enquanto ainda apresenta a interface que números inteiros são objetos com métodos, hierarquia de superclasses, etc.). O problema é que um compilador para Python não podesaiba disso, porque as classes são definidas em tempo de execução, podem ser modificadas em tempo de execução e até os módulos que definem e importam são resolvidos em tempo de execução (e mesmo quais instruções de importação são executadas depende de coisas que só podem ser conhecidas em tempo de execução). Portanto, o compilador Python precisaria saber qual código foi executado (por exemplo, resolver o problema da parada) para saber o que a instrução que ele está compilando fará.

Portanto, mesmo com as análises mais sofisticadas teoricamente possíveis , você simplesmente não pode dizer muito sobre o que uma determinada instrução Python fará antes do tempo. Isso significa que, mesmo que um compilador Python sofisticado fosse implementado, em quase todos os casos ainda seria necessário emitir código de máquina que segue o protocolo de pesquisa de dicionário do Python para determinar a classe de um objeto e encontrar métodos (percorrendo o MRO da hierarquia de classes, que também pode mudar dinamicamente no tempo de execução e, portanto, é difícil compilar em uma tabela de método virtual simples) e basicamente fazer o que os intérpretes (lentos) fazem. É por isso que não há realmente nenhum compilador de otimização sofisticado para linguagens dinâmicas. Não é apenas difícil criar um, o máximo retorno possível não é '

Observe que isso não se baseia no que o código está fazendo, mas no que o código poderia estar fazendo. Mesmo o código Python, que é uma série simples de operações aritméticas inteiras, deve ser compilado como se estivesse invocando operações de classe arbitrárias. Linguagens estáticas têm maiores restrições sobre as possibilidades do que o código poderia estar fazendo e, conseqüentemente, seus compiladores podem fazer mais suposições.

Os compiladores JIT ganham com isso esperando até o tempo de execução compilar / otimizar. Isso os permite emitir um código que funciona para o que o código está fazendo e não para o que poderia estar fazendo. E por causa disso, os compiladores JIT têm um retorno potencial muito maior para linguagens dinâmicas do que para linguagens estáticas; para linguagens mais estáticas, muito do que um otimizador gostaria de saber pode ser conhecido com antecedência, portanto é melhor otimizar isso, deixando menos para um compilador JIT.

Existem vários compiladores JIT para linguagens dinâmicas que pretendem atingir velocidades de execução comparáveis ​​às do C / C ++ compilado e otimizado. Existem até otimizações que podem ser feitas por um compilador JIT que não pode ser feito por um compilador antecipado para qualquer idioma; portanto, teoricamente, a compilação JIT (para alguns programas) poderia um dia superar o melhor compilador estático possível. Porém, como Devin apontou corretamente, as propriedades da compilação JIT (somente os "pontos de acesso" são rápidos e somente após um período de aquecimento) significa que é improvável que as linguagens dinâmicas compiladas por JIT sejam adequadas para todas as aplicações possíveis, mesmo que se tornem mais rápido ou mais rápido que os idiomas compilados estaticamente em geral.


11
Agora são dois votos negativos, sem comentários. Gostaria de receber sugestões de como melhorar esta resposta!
Ben

Não diminuí a votação, mas você está incorreto quanto à "necessidade de resolver o problema da parada". Foi demonstrado em muitas circunstâncias que o código em linguagens dinâmicas pode ser compilado para obter o código de destino ideal, enquanto que, pelo que
sei

@ Mikera Desculpe, mas não, você está incorreto. Ninguém jamais implementou um compilador (no sentido em que entendemos que o GCC é um compilador) para Python totalmente geral ou outras linguagens dinâmicas. Todo sistema desse tipo funciona apenas para um subconjunto da linguagem, ou apenas para certos programas, ou às vezes emite código que é basicamente um intérprete que contém um programa codificado. Se você quiser, escreverei um programa Python contendo a linha em foo = x + yque a previsão do comportamento do operador de adição no tempo de compilação depende da solução do problema de parada.
8553 Ben

Estou certo e acho que você está perdendo o objetivo. Eu disse "em muitas circunstâncias". Eu não disse "em todas as circunstâncias possíveis". Se você pode ou não construir um exemplo artificial vinculado ao problema da parada é irrelevante no mundo real. FWIW, você também pode construir um exemplo semelhante para C ++ para não provar nada. Enfim, eu não vim aqui para entrar em uma discussão, apenas para sugerir melhorias na sua resposta. É pegar ou largar.
Mikera

@ Mikea Eu acho que você pode estar perdendo o ponto. Para compilar x + yoperações eficientes de adição de máquina, é necessário saber em tempo de compilação se é ou não. O tempo todo , não apenas o tempo todo. Para linguagens dinâmicas, isso quase nunca é possível com programas realistas, embora heurísticas razoáveis ​​achem acertadas a maior parte do tempo. A compilação requer garantias em tempo de compilação . Então, falando sobre "em muitas circunstâncias", na verdade você não está respondendo à minha resposta.
Ben

9

Apenas um ponteiro rápido que descreve o pior cenário possível para linguagens dinâmicas:

análise Perl não é computável

Como conseqüência, o Perl (completo) nunca pode ser compilado estaticamente.


Em geral, como sempre, depende. Estou confiante de que, se você tentar emular recursos dinâmicos em uma linguagem compilada estaticamente, interpretadores bem concebidos ou variantes (parcialmente) compiladas poderão se aproximar ou diminuir o desempenho de linguagens compiladas estaticamente.

Outro ponto a ser lembrado é que as linguagens dinâmicas resolvem outro problema que não o C. C é pouco mais que uma sintaxe agradável para o assembler, enquanto as linguagens dinâmicas oferecem abstrações ricas. O desempenho do tempo de execução geralmente não é a principal preocupação: o tempo de colocação no mercado, por exemplo, depende da capacidade de seus desenvolvedores criarem sistemas complexos e de alta qualidade em curtos prazos. Extensibilidade sem recompilação, por exemplo, com plugins, é outro recurso popular. Qual idioma você prefere nesses casos?


5

Na tentativa de oferecer uma resposta mais objetivamente científica a essa questão, defendo o seguinte. Uma linguagem dinâmica requer que um intérprete, ou tempo de execução, tome decisões em tempo de execução. Esse intérprete, ou tempo de execução, é um programa de computador e, como tal, foi escrito em alguma linguagem de programação, estática ou dinâmica.

Se o intérprete / tempo de execução foi escrito em uma linguagem estática, pode-se escrever um programa nessa linguagem estática que (a) desempenha a mesma função que o programa dinâmico que ele interpreta e (b) executa pelo menos também. Felizmente, isso é evidente, pois fornecer uma prova rigorosa dessas reivindicações exigiria um esforço adicional (possivelmente considerável).

Supondo que essas afirmações sejam verdadeiras, a única saída é exigir que o intérprete / tempo de execução também seja escrito em uma linguagem dinâmica. No entanto, encontramos o mesmo problema de antes: se o intérprete é dinâmico, é necessário um intérprete / tempo de execução, que também deve ter sido escrito em uma linguagem de programação, dinâmica ou estática.

A menos que você assuma que uma instância de um intérprete é capaz de se interpretar em tempo de execução (espero que isso seja evidentemente absurdo), a única maneira de superar linguagens estáticas é que cada instância de intérprete seja interpretada por uma instância de intérprete separada; isso leva a uma regressão infinita (espero que isso seja evidentemente absurdo) ou a um ciclo fechado de intérpretes (espero que isso também seja evidentemente absurdo).

Parece, então, que mesmo em teoria, as linguagens dinâmicas podem ter um desempenho melhor do que as linguagens estáticas, em geral. Ao usar modelos de computadores realistas, parece ainda mais plausível; afinal, uma máquina pode executar apenas seqüências de instruções da máquina e todas as seqüências de instruções da máquina podem ser compiladas estaticamente.

Na prática, combinar o desempenho de uma linguagem dinâmica com uma linguagem estática pode exigir a reimplementação do intérprete / tempo de execução em uma linguagem estática; no entanto, o fato de você poder fazer isso é o ponto crucial deste argumento. É uma questão de galinha e ovo e, desde que você concorde com as suposições não comprovadas (embora, na minha opinião, principalmente evidentes) feitas acima, podemos realmente respondê-la; temos que concordar com as linguagens estáticas, não dinâmicas.

Outra maneira de responder à pergunta, à luz dessa discussão, é a seguinte: no programa armazenado, controle = modelo de dados da computação, que está no coração da computação moderna, a distinção entre compilação estática e dinâmica é uma falsa dicotomia; linguagens estaticamente compiladas devem ter um meio de gerar e executar código arbitrário em tempo de execução. Está fundamentalmente relacionado à computação universal.


Relendo isso, não acho que seja verdade. A compilação JIT interrompe seu argumento. Mesmo o código mais simples, por exemplo, main(args) { for ( i=0; i<1000000; i++ ) { if ( args[0] == "1" ) {...} else {...} }pode acelerar significativamente quando o valor de argsé conhecido (assumindo que ele nunca muda, o que podemos afirmar). Um compilador estático não pode criar código que elimine a comparação. (É claro que, nesse exemplo que você acabou de puxar o iffora do circuito Mas a coisa pode ser mais complicado..)
Raphael

@ Rafael, acho que JIT provavelmente faz o meu argumento. Os programas que fazem compilação JIT (por exemplo, JVM) geralmente são programas compilados estaticamente. Se um programa JIT compilado estaticamente puder executar um script mais rapidamente do que outro programa estático, faça o mesmo trabalho, apenas "empacote" o script com o compilador JIT e chame o pacote como programa compilado estaticamente. Isso deve funcionar pelo menos tão bem quanto o JIT operando em um programa dinâmico separado, contradizendo qualquer argumento de que o JIT deve fazer melhor.
precisa saber é o seguinte

Hum, é como dizer que agrupar um script Ruby com seu intérprete fornece um programa estaticamente compilado. Não concordo (já que remove todas as distinções de idiomas a esse respeito), mas isso é uma questão de semântica, não de conceitos. Conceitualmente, adaptar o programa em tempo de execução (JIT) pode fazer otimizações que você não pode fazer em tempo de compilação (compilador estático), e esse é o meu ponto.
Raphael

@Raphael Que não há uma distinção significativa é o ponto da resposta: qualquer tentativa de classificar rigidamente algumas linguagens como estáticas e, portanto, sofrendo com limitações de desempenho, falha exatamente por esse motivo: não há uma diferença convincente entre um pré-empacotado (Ruby , script) e um programa em C. Se (Ruby, script) pode fazer com que a máquina execute a sequência correta de instruções para resolver com eficiência um determinado problema, o mesmo poderia acontecer com um programa C habilmente criado.
Patrick87

Mas você pode definir a diferença. Uma variante envia o código em mãos para o processador inalterado (C), a outra compila em tempo de execução (Ruby, Java, ...). O primeiro é o que entendemos por "compilação estática", enquanto o último seria "compilação just in time" (que permite otimizações dependentes de dados).
Raphael

4

Você pode criar compiladores para linguagens dinâmicas como Ruby para ter desempenho semelhante e comparável ao C / C ++?

Eu acho que a resposta é "sim" . Eu também acredito que eles podem até superar a arquitetura C / C ++ atual em termos de eficiência (mesmo que ligeiramente).

O motivo é simples: há mais informações em tempo de execução do que em tempo de compilação.

Tipos dinâmicos são apenas um pequeno obstáculo: se uma função é sempre ou quase sempre executada com os mesmos tipos de argumentos, um otimizador JIT pode gerar um código de máquina e ramificação para esse caso específico. E há muito mais que pode ser feito.

Veja Dynamic Languages ​​Strike Back , um discurso de Steve Yegge, do Google (também há uma versão em vídeo em algum lugar em que acredito). Ele menciona algumas técnicas concretas de otimização de JIT da V8. Inspirador!

Estou ansioso pelo que teremos nos próximos 5 anos!


2
Eu amo o otimismo.
31812 Dave Clarke

Acredito que houve algumas críticas muito específicas de imprecisões na palestra de Steve. Vou publicá-los quando os encontrar.
Konrad Rudolph

11
@DaveClarke isso é o que me mantém funcionando :)
Kos

2

Pessoas que aparentemente pensam que isso é teoricamente possível, ou em um futuro distante, estão completamente erradas na minha opinião. O ponto reside no fato de que as linguagens dinâmicas fornecem e impõem um estilo de programação totalmente diferente. Na verdade, a diferença é dupla, mesmo se os dois aspectos estiverem inter-relacionados:

  • Os símbolos (vars, ou melhor, id <-> ligações de dados de todos os tipos) não são digitados.
  • Estruturas (os dados, tudo o que vive em tempo de execução) também não são digitados pelos tipos de seus elementos.

O segundo ponto fornece genericidade gratuitamente. Observe que as estruturas aqui são elementos compostos, coleções, mas também os próprios tipos, e até rotinas (!) De todos os tipos (funções, ações, operações) ... Poderíamos digitar estruturas por seus tipos de elementos, mas devido ao primeiro ponto o verificação aconteceria em tempo de execução de qualquer maneira. Poderíamos ter digitado símbolos e ainda ter estruturado aqueles não digitados de acordo com seus tipos de elementos (uma matriz aseria apenas digitada como uma matriz e não como uma matriz de ints), mas mesmo essas poucas não são verdadeiras em uma linguagem dinâmica (também apoderiam conter uma linha).

O melhor desempenho que podemos obter na programação dinâmica é, na minha opinião, equivalente ao seguinte: implementar em C o modelo de uma linguagem dinâmica, vamos chamá-lo de , isto é, um tipo de máquina fictícia ou uma biblioteca de tempo de execução completa. E, em seguida, programe (em C diretamente) usando apenas o modelo, sem recursos simples de C. Isso significa ter:L

  • um tipo totalmente polimórfico (C) Element, que inclui anotações do tipo (ou uma referência à representação real do tipo de no modelo)LLL
  • todos os símbolos são desse tipo Element, eles podem conter elementos de qualquer tipoL
  • todas as estruturas (novamente, incluindo rotinas de modelo) recebem apenas elementos

Está claro para mim que isso é apenas uma enorme penalidade de perf; e eu nem toco todas as consequências (a miríade de verificações de tempo de execução de todos os tipos necessários para garantir a sensibilidade do programa) bem descritas em outras postagens.


+1 muito interessante. Você leu minha resposta? Você e meu pensamento parecem semelhantes, embora sua resposta forneça mais detalhes e uma perspectiva interessante.
precisa saber é o seguinte

Os idiomas dinâmicos não precisam ser digitados. A implementação de um modelo de linguagem dinâmica em C restringe severamente as possibilidades de otimização; torna muito difícil para o compilador reconhecer otimizações de alto nível (por exemplo, dados imutáveis) e força algumas operações fundamentais (por exemplo, chamadas de função) a passar por C. O que você descreve não está longe de ser um interpretador de bytecode, com a decodificação de bytecode pré-avaliado; compiladores nativos tendem a ter um desempenho significativamente melhor, duvido que a decodificação de bytecode possa justificar a diferença.
Gilles 'SO- stop be evil'

@ Patrick87: você está certo, nossas linhas de pensamento parecem muito semelhantes (não tinha lido antes, desculpe, minha reflexão vem da implementação atual de um dyn lang em C).
SPIR

@ Gilles: Eu concordo com "... não precisa ser digitado ", se você quer dizer que não está estaticamente digitado. Mas não é isso que as pessoas pensam dos dyn langs em geral, eu acho. Pessoalmente, considero a genéricos (no sentido geral dado na resposta acima) um recurso muito mais poderoso e muito mais difícil de viver sem. Podemos encontrar facilmente maneiras de lidar com tipos estáticos, ampliando nosso pensamento sobre como um determinado tipo (aparentemente polimórfico) pode ser definido ou dando mais flexibilidade às instâncias diretamente.
SPIR

1

Não tive tempo de ler todas as respostas em detalhes ... mas fiquei divertido.

Houve uma controvérsia semelhante nos anos sessenta e início dos anos setenta (a história da ciência da computação se repete frequentemente): podem ser compiladas linguagens de alto nível para produzir código tão eficiente quanto o código da máquina, digamos, código de montagem, produzido manualmente por um programador. Todo mundo sabe que um programador é muito mais esperto do que qualquer programa e pode ter uma otimização muito inteligente (pensando na maior parte do que agora é chamado de otimização do olho mágico). É claro que isso é ironia da minha parte.

Havia até um conceito de expansão de código: a proporção entre o tamanho do código produzido por um compilador e o tamanho do código para o mesmo programa produzido por um bom programador (como se houvesse muitos deles :-). Obviamente, a idéia era que essa proporção fosse sempre maior que 1. As línguas da época eram Cobol e Fortran 4, ou Algol 60 para os intelectuais. Eu acredito que Lisp não foi considerado.

Bem, havia alguns rumores de que alguém havia produzido um compilador que às vezes podia obter uma taxa de expansão de 1 ... até que simplesmente se tornou a regra que o código compilado era muito melhor do que o código escrito à mão (e mais confiável também). As pessoas estavam preocupadas com o tamanho do código naquela época (pequenas memórias), mas o mesmo vale para velocidade ou consumo de energia. Eu não vou entrar nas razões.

Recursos estranhos, recursos dinâmicos de um idioma não importam. O que importa é como eles são usados, se são usados. O desempenho, em qualquer unidade (tamanho do código, velocidade, energia, ...) geralmente depende de partes muito pequenas dos programas. Portanto, há uma boa chance de as instalações que dão poder expressivo não atrapalharem realmente. Com as boas práticas de programação, as instalações avançadas são usadas apenas de maneira disciplinada, para imaginar novas estruturas (essa foi a lição cega).

O fato de um idioma não ter tipagem estática nunca significou que os programas escritos nesse idioma não sejam digitados estaticamente. Por outro lado, pode ser que o sistema de tipos usado por um programa ainda não esteja suficientemente formalizado para que um verificador de tipos exista agora.

Houve, na discussão, várias referências à análise de pior caso ("problema de parada", análise PERL). Mas a análise do pior caso é principalmente irrelevante. O que importa é o que acontece na maioria dos casos ou em casos úteis ... por mais definidos, compreendidos ou experientes. Aí vem outra história, diretamente relacionada à otimização do programa. Isso aconteceu há muito tempo em uma grande universidade do Texas, entre um estudante de doutorado e seu orientador (que mais tarde foi eleito em uma das academias nacionais). Pelo que me lembro, o aluno insistiu em estudar um problema de análise / otimização que o orientador demonstrou ser intratável. Logo eles não estavam mais falando. Mas o aluno estava certo: o problema era tratável o suficiente na maioria dos casos práticos, de modo que a dissertação que ele produziu se tornou um trabalho de referência.

E para comentar ainda mais a afirmação de que Perl parsing is not computable, independentemente do significado dessa frase, existe um problema semelhante com o ML, que é uma linguagem notavelmente bem formalizada. Type checking complexity in ML is a double exponential in the lenght of the program.Esse é um resultado muito preciso e formal na pior das hipóteses ... o que não importa. No entanto, os usuários de ML ainda estão esperando por um programa prático que exploda o verificador de tipos.

Em muitos casos, como era antes, o tempo e a competência humana são mais escassos que o poder computacional.

O verdadeiro problema do futuro será evoluir nossas linguagens para integrar novos conhecimentos, novas formas de programação, sem precisar reescrever todo o software legado que ainda é usado.

Se você olhar para a matemática, é um corpo muito grande de conhecimentos. As linguagens usadas para expressá-lo, notações e conceitos evoluíram ao longo dos séculos. É fácil escrever teoremas antigos com os novos conceitos. Nós adaptamos as principais provas, mas não nos preocupamos com muitos resultados.

Porém, no caso da programação, talvez seja necessário reescrever todas as provas do zero (programas são provas). Pode ser que o que realmente precisamos seja de linguagens de programação evolutivas e de muito alto nível. Os designers do otimizador terão prazer em seguir.


0

Algumas notas:

  • Nem todos os idiomas de alto nível são dinâmicos. Haskell é de nível muito alto, mas é totalmente digitado estaticamente. Mesmo linguagens de programação de sistemas como Rust, Nim e D podem expressar abstrações de alto nível de maneira sucinta e eficiente. De fato, eles podem ser tão concisos quanto as linguagens dinâmicas.

  • Existem compiladores antecipadamente altamente otimizados para linguagens dinâmicas. As boas implementações de Lisp atingem metade da velocidade do equivalente C.

  • A compilação JIT pode ser uma grande vitória aqui. O Cloud Application Firewall do CloudFlare gera código Lua que é executado pelo LuaJIT. O LuaJIT otimiza bastante os caminhos de execução realmente usados ​​(normalmente, os caminhos sem ataque), com o resultado de que o código é executado muito mais rápido que o código produzido por um compilador estático na carga de trabalho real. Ao contrário de um compilador estático com otimização guiada por perfil, o LuaJIT se adapta às mudanças nos caminhos de execução no tempo de execução.

  • A desoptimização também é crucial. Em vez de o código compilado por JIT precisar verificar se uma classe está sendo monkeypatched, o ato de monkeypatching aciona um gancho no sistema de tempo de execução que descarta o código da máquina que dependia da definição antiga.


Como isso é uma resposta? Bem, bala três, talvez, se você adicionou referências.
Raphael

Sou muito cético em relação à afirmação de que o PGO não poderia corresponder ao desempenho do LuaJIT para código de aplicativo da web sob carga de trabalho típica.
9289 Konrad Rudolph #

@KonradRudolph A principal vantagem de um JIT é que o JIT adapta o código à medida que diferentes caminhos ficam quentes.
Demi

@ Demetri eu sei disso. Mas é muito difícil quantificar se isso é uma vantagem - veja minha resposta e a discussão dos comentários. Em poucas palavras: embora o JIT possa se adaptar às mudanças no uso, ele também precisa rastrear coisas em tempo de execução, o que gera uma sobrecarga. O ponto de equilíbrio para isso é intuitivamente apenas onde ocorrem mudanças frequentes no comportamento. Para aplicativos da web, provavelmente existe apenas um único (ou muito poucos) padrão de uso pelo qual a otimização compensa; portanto, o aumento mínimo de desempenho devido à adaptabilidade não compensa a sobrecarga da criação de perfil contínuo.
9609 Konrad Rudolph
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.