Por que os idiomas de alto nível aparentemente nunca alcançam idiomas de baixo nível em termos de velocidade? Exemplos de linguagens de alto nível seriam Python, Haskell e Java. Linguagens de baixo nível seriam mais difíceis de definir, mas digamos C. Comparações podem ser encontradas em toda a Internet e todas concordam que C é muito mais rápido, às vezes por um fator de 10 ou mais.
O que causa uma diferença tão grande no desempenho e por que as linguagens de alto nível não conseguem acompanhar?
Inicialmente, eu acreditava que tudo isso é culpa dos compiladores e que as coisas melhorarão no futuro, mas algumas das linguagens de nível superior mais populares existem há décadas e ainda ficam para trás quando se trata de velocidade. Eles não podem simplesmente compilar com uma árvore de sintaxe semelhante à C e seguir os mesmos procedimentos que geram o código da máquina? Ou talvez tenha algo a ver com a sintaxe em si?
Exemplos:
Respostas:
Desmistificando alguns mitos
Não existe um idioma rápido. Um idioma geralmente pode produzir código rápido, mas idiomas diferentes se destacam em diferentes benchmarks. Podemos classificar idiomas em um conjunto específico de benchmarks defeituosos, mas não podemos classificar idiomas no vácuo.
O código C tende a ser mais rápido porque as pessoas que precisam de cada centímetro de desempenho usam C. Uma estatística de que C é mais rápido "por um fator de 10" pode ser imprecisa, porque pode ser que as pessoas que usam Python simplesmente não se importam tanto. sobre velocidade e não escrevemos o código Python ideal. Vemos isso em particular em idiomas como Haskell. Se você se esforçar muito , pode escrever Haskell com desempenho igual ao C. Mas a maioria das pessoas não precisa desse desempenho, por isso temos várias comparações falhas.
Às vezes, é inseguro, não abstração, que faz C rápido. Sua falta de verificação de limites de matriz e ponteiro nulo economiza tempo e tem sido a causa de inúmeras falhas de segurança ao longo dos anos.
Os idiomas não são rápidos, as implementações são rápidas. Muitas linguagens abstratas começam devagar, porque a velocidade não é seu objetivo, mas ficam mais rápidas à medida que mais e mais otimizações são adicionadas.
A troca de abstração versus velocidade é talvez imprecisa. Eu sugeriria uma comparação melhor:
Simplicidade, velocidade, abstração: escolha dois.
Se estivermos executando algoritmos idênticos em diferentes idiomas, a questão da velocidade se resume ao problema de "Quantas coisas precisamos fazer em tempo de execução para fazer isso funcionar?"
Em uma linguagem altamente abstrata que é simples , como Python ou JavaScript, porque não sabemos sobre a representação dos dados até o tempo de execução, acaba havendo muitas referências ao ponteiro e verificações dinâmicas acontecendo no tempo de execução, que são lentas.
Da mesma forma, há muitas verificações que precisam ser feitas para garantir que você não destrua seu computador. Quando você chama uma função em python, ele precisa garantir que o objeto que você está chamando seja uma função, pois, caso contrário, você poderá executar código aleatório que faz coisas terríveis.
Finalmente, a maioria das linguagens abstratas tem sobrecarga da coleta de lixo. A maioria dos programadores concordou que ter que rastrear a vida útil da memória alocada dinamicamente é uma dor e eles preferem que um coletor de lixo faça isso por eles em tempo de execução. Isso leva tempo, para que um programa C não precise gastar no GC.
Resumo Não significa lento
No entanto, existem idiomas que são abstratos e rápidos. O mais dominante nesta era é a ferrugem. Ao introduzir um verificador de empréstimos e um sistema sofisticado de tipos, ele permite código abstrato e usa informações em tempo de compilação para reduzir a quantidade de trabalho que precisamos realizar em tempo de execução (por exemplo, coleta de lixo).
Qualquer linguagem digitada estaticamente economiza tempo, reduzindo o número de verificações em tempo de execução e introduz complexidade, exigindo que agrademos um verificador de letras em tempo de compilação. Da mesma forma, se tivermos uma linguagem que codifique se um valor pode ser nulo em seu sistema de tipos, podemos economizar tempo com verificações de ponteiro nulo em tempo de compilação.
fonte
Aqui estão algumas idéias importantes sobre isso.
uma comparação fácil / estudo de caso para fazer abstração wrt x velocidade é java vs c ++. O java foi projetado para abstrair alguns dos aspectos de nível inferior do c ++, como gerenciamento de memória. nos primeiros dias (em torno da invenção da linguagem em meados dos anos 90), a detecção de lixo de java não era muito rápida, mas após algumas décadas de pesquisa, os coletores de lixo eram extremamente ajustados / rápidos / otimizados, de modo que os coletores de lixo eram amplamente eliminados como dreno de desempenho em java. por exemplo, veja até este título de 1998: Os testes de desempenho mostram Java tão rápido quanto C ++ / javaworld
linguagens de programação e sua longa evolução têm uma "estrutura piramidal / hierárquica" inerente como uma espécie de padrão de design transcendente. no topo da pirâmide há algo que controla outras seções inferiores da pirâmide. em outras palavras, os blocos de construção são feitos de blocos de construção. isso também pode ser visto na estrutura da API. nesse sentido, uma maior abstração sempre leva a algum novo componente no topo da pirâmide, controlando outros componentes. então, em certo sentido, não é tanto que todas as línguas estejam niveladas uma com a outra, mas que as línguas exigem rotinas em outras línguas. por exemplo, muitas linguagens de script (python / ruby) costumam chamar bibliotecas C ou C ++, rotinas numéricas ou de matriz são um exemplo típico disso. portanto, existem idiomas de nível superior e inferior e os idiomas de nível superior chamam os idiomas de nível inferior, por assim dizer. nesse sentido, medir a velocidade relativa não é realmente comparável.
pode-se dizer que novas linguagens estão sendo inventadas o tempo todo para tentar otimizar a troca de abstração / velocidade, ou seja, seu principal objetivo de design. talvez não seja tanto que uma maior abstração sempre sacrifique a velocidade, mas que um equilíbrio melhor esteja sempre sendo procurado com designs mais novos. por exemplo, o Google Go foi otimizado de várias maneiras especificamente com o compromisso em mente, para ser simultaneamente de alto nível e desempenho. veja, por exemplo, Google Go: Por que a linguagem de programação do Google pode rivalizar com Java na empresa / mundo da tecnologia
fonte
O jeito que eu gosto de pensar em desempenho é "onde a borracha encontra a estrada". O computador executa instruções, não abstrações.
O que eu quero ver é o seguinte: toda instrução executada está "ganhando força", contribuindo substancialmente para o resultado final? Como um exemplo muito simples, considere procurar uma entrada em uma tabela com 1024 entradas. Esse é um problema de 10 bits porque o programa precisa "aprender" 10 bits antes de saber a resposta. Se o algoritmo for pesquisa binária, cada iteração contribui com 1 bit de informação, porque reduz a incerteza em um fator de 2. Portanto, são necessárias 10 iterações, uma para cada bit.
A pesquisa linear, por outro lado, é inicialmente muito ineficiente porque as primeiras iterações reduzem a incerteza por um fator muito pequeno. Portanto, eles não estão aprendendo muito pelo esforço despendido.
OK, então se o compilador pode permitir que o usuário agrupe boas instruções de uma maneira considerada "abstrata", tudo bem.
fonte
Por sua natureza, a abstração reduz a comunicação de informações, tanto para o programador quanto para as camadas inferiores do sistema (compilador, bibliotecas e sistema de tempo de execução). A favor da abstração, isso geralmente permite que as camadas inferiores suponham que o programador não esteja preocupado com nenhum comportamento não especificado, fornecendo maior flexibilidade no fornecimento do comportamento especificado.
Um exemplo de um benefício potencial desse aspecto "não me importo" está no layout dos dados. Em C (baixa abstração), o compilador é mais restrito nas otimizações de layout de dados. Mesmo que o compilador possa discernir (por exemplo, através de informações de perfil) que otimizações quentes / frias ou falsas para evitar o compartilhamento seriam benéficas, geralmente é impedido de aplicá-las. (Existe alguma liberdade em especificar "como se", ou seja, tratar a especificação de maneira mais abstrata, mas derivar todos os possíveis efeitos colaterais acrescenta um ônus ao compilador.)
Uma especificação mais abstrata também é mais robusta contra mudanças nas trocas e usos. As camadas inferiores são menos restritas ao otimizar o programa para novas características do sistema ou novos usos. Uma especificação mais concreta deve ser reescrita por um programador ou um esforço adicional deve ser feito pelas camadas inferiores para garantir o comportamento "como se".
O aspecto que dificulta o desempenho da abstração oculta de informações é "não é possível expressar", que as camadas inferiores normalmente tratam como "não sabem". Isso significa que as camadas inferiores devem discernir informações úteis para otimização de outros meios, como uso geral típico, uso direcionado ou informações de perfil específicas.
O impacto da ocultação de informações também funciona na outra direção. O programador pode ser mais produtivo por não ter que considerar e especificar todos os detalhes, mas o programador pode ter menos informações sobre o impacto de opções de design de nível superior.
Por outro lado, quando o código é mais específico (menos abstrato), as camadas inferiores do sistema podem mais simplesmente fazer o que lhes é pedido que façam da mesma maneira que são instruídas a fazê-lo. Se o código for bem escrito para seu uso direcionado, ele se ajustará bem ao seu uso direcionado. Uma linguagem menos abstrata (ou paradigma de programação) permite que o programador otimize a implementação por design detalhado e pelo uso de informações que não são facilmente comunicadas em uma determinada linguagem às camadas inferiores.
Como foi observado, linguagens menos abstratas (ou técnicas de programação) são atraentes quando habilidades e esforços adicionais do programador podem produzir resultados que valem a pena. Quando mais esforço e habilidade do programador são aplicados, os resultados normalmente serão melhores. Além disso, um sistema de linguagem usado menos para aplicativos críticos para o desempenho (em vez de enfatizar o esforço ou a confiabilidade do desenvolvimento - as verificações de limites e a coleta de lixo não são apenas sobre a produtividade do programador, mas sobre a correção, a abstração que reduz a carga mental no programador pode melhorar a confiabilidade) terá menos pressão para melhorar o desempenho.
A especificidade também funciona contra o princípio de não se repetir, porque a otimização geralmente é possível adaptando o código a um uso específico. Isso tem confiabilidade óbvia e implicações no esforço de programação.
As abstrações fornecidas por um idioma também podem incluir trabalhos indesejados ou desnecessários, sem meios para escolher uma abstração menos pesada. Embora o trabalho desnecessário possa às vezes ser descoberto e removido pelas camadas inferiores (por exemplo, verificações de limites podem ser extraídas do corpo de um loop e totalmente removidas em alguns casos), determinar que essa é uma otimização válida requer mais "habilidade e esforço" o compilador.
A idade e a popularidade do idioma também são fatores dignos de nota, tanto na disponibilidade de programadores qualificados quanto na qualidade das camadas inferiores do sistema (incluindo bibliotecas maduras e exemplos de código).
Outro fator conflitante nessas comparações é a diferença ortogonal entre a compilação antecipada e a just-in-time. Embora a compilação just-in-time possa explorar mais facilmente as informações de perfil (sem depender do programador para fornecer execuções de perfil) e a otimização específica do sistema (a compilação antecipada pode ter como objetivo uma compatibilidade mais ampla), a sobrecarga da otimização agressiva é contabilizada como parte do desempenho do tempo de execução. Os resultados do JIT podem ser armazenados em cache, reduzindo a sobrecarga do código usado com frequência. (A alternativa de reoptimização binária pode fornecer algumas vantagens da compilação JIT, mas os formatos tradicionais de distribuição binária descartam a maioria das informações de código-fonte potencialmente forçando o sistema a tentar discernir a intenção de uma implementação específica.)
(Linguagens de abstração mais baixas, devido à ênfase no controle do programador, favorecem o uso da compilação antecipada. A compilação no tempo de instalação pode ser tolerada, embora a seleção da implementação no tempo do link forneça maior controle do programador. A compilação JIT sacrifica o controle significativo. )
Há também a questão da metodologia de benchmarking. Esforço / habilidade iguais são efetivamente impossíveis de estabelecer, mas mesmo assim poderiam ser alcançados, os objetivos da linguagem influenciam os resultados. Se um tempo de programação máximo baixo for necessário, um programa para uma linguagem menos abstrata pode até falhar em ser completamente escrito em comparação com uma expressão idiomática simples em uma linguagem mais abstrata. Se um tempo / esforço máximo alto de programação fosse permitido, as linguagens de baixa abstração teriam uma vantagem. Os benchmarks que apresentam os melhores resultados seriam naturalmente tendenciosos em favor de linguagens menos abstratas.
Às vezes é possível programar de uma maneira menos idiomática em uma linguagem para obter as vantagens de outros paradigmas de programação, mas mesmo quando o poder expressivo está disponível, as vantagens e desvantagens de fazer isso podem não ser favoráveis.
fonte