O que confirma a afirmação de que o C ++ pode ser mais rápido que uma JVM ou CLR com JIT? [fechadas]


119

Um tema recorrente no SE que notei em muitas perguntas é o argumento contínuo de que o C ++ é mais rápido e / ou mais eficiente do que linguagens de nível superior como Java. O contra-argumento é que a JVM ou CLR moderna pode ser igualmente eficiente, graças ao JIT e assim por diante, para um número crescente de tarefas e que o C ++ é cada vez mais eficiente se você sabe o que está fazendo e por que fazer as coisas de uma certa maneira merecerá aumentos de desempenho. Isso é óbvio e faz todo o sentido.

Gostaria de saber uma explicação básica (se é que existe ...) sobre o porquê e como certas tarefas são mais rápidas em C ++ que a JVM ou CLR? É simplesmente porque o C ++ é compilado no código da máquina, enquanto a JVM ou o CLR ainda tem a sobrecarga de processamento da compilação do JIT em tempo de execução?

Quando tento pesquisar o tópico, tudo o que encontro são os mesmos argumentos que descrevi acima, sem informações detalhadas sobre como entender exatamente como o C ++ pode ser utilizado para computação de alto desempenho.


O desempenho também depende da complexidade do programa.
pandu

23
Eu acrescentaria que "C ++ é apenas cada vez mais eficiente se você sabe o que está fazendo e por que fazer as coisas de uma certa maneira merece um aumento de desempenho". dizendo que não é apenas uma questão de conhecimento, é uma questão de tempo do desenvolvedor. Nem sempre é eficiente maximizar a otimização. É por isso que linguagens de nível superior, como Java e Python, existem (entre outros motivos) - para diminuir a quantidade de tempo que um programador gasta em programação para realizar uma determinada tarefa às custas da otimização altamente ajustada.
Joel Cornett

4
@ Joel Cornett: Eu concordo totalmente. Definitivamente, sou mais produtivo em Java do que em C ++ e só considero C ++ quando preciso escrever um código muito rápido. Por outro lado, vi o código C ++ mal escrito sendo realmente lento: o C ++ é menos útil nas mãos de programadores não qualificados.
Giorgio

3
Qualquer saída de compilação que possa ser produzida por um JIT pode ser produzida pelo C ++, mas o código que o C ++ pode produzir pode não ser necessariamente produzido por um JIT. Portanto, os recursos e as características de desempenho do C ++ são um superconjunto dos de qualquer linguagem de nível superior. QED
tylerl

1
@Doval Tecnicamente verdade, mas como regra, você pode contar os possíveis fatores de tempo de execução que afetam o desempenho de um programa, por um lado. Geralmente sem usar mais de dois dedos. Na pior das hipóteses, você envia vários binários ... exceto que você nem precisa fazer isso porque a potencial aceleração é insignificante, e é por isso que ninguém se incomoda.
tylerl

Respostas:


200

É tudo sobre a memória (não o JIT). A vantagem do JIT sobre C é limitada principalmente à otimização de chamadas virtuais ou não virtuais através de inlining, algo que o BTB da CPU já está trabalhando duro para fazer.

Nas máquinas modernas, o acesso à RAM é muito lento (comparado a qualquer coisa que a CPU faz), o que significa que os aplicativos que usam os caches o máximo possível (que é mais fácil quando menos memória é usada) podem ser até cem vezes mais rápidos do que aqueles que não. E há muitas maneiras pelas quais o Java usa mais memória que o C ++ e dificulta a gravação de aplicativos que exploram completamente o cache:

  • Há uma sobrecarga de memória de pelo menos 8 bytes para cada objeto, e o uso de objetos em vez de primitivos é necessário ou preferido em muitos locais (nomeadamente as coleções padrão).
  • Strings consistem em dois objetos e têm uma sobrecarga de 38 bytes
  • O UTF-16 é usado internamente, o que significa que cada caractere ASCII requer dois bytes em vez de um (a Oracle JVM introduziu recentemente uma otimização para evitar isso para cadeias ASCII puras).
  • Não há um tipo de referência agregado (ou seja, estruturas) e, por sua vez, não há matrizes de tipos de referência agregados. Um objeto Java, ou matriz de objetos Java, tem uma localização de cache L1 / L2 muito ruim em comparação com estruturas C e matrizes.
  • Os genéricos Java usam apagamento de tipo, que possui uma localidade de cache ruim em comparação à instanciação de tipo.
  • A alocação de objetos é opaca e deve ser feita separadamente para cada objeto, portanto, é impossível para um aplicativo dispor deliberadamente seus dados de maneira amigável ao cache e ainda tratá-los como dados estruturados.

Alguns outros fatores relacionados à memória, mas não ao cache:

  • Não há alocação de pilha, portanto, todos os dados não primitivos com os quais você trabalha devem estar na pilha e passar pela coleta de lixo (alguns JITs recentes realizam a alocação de pilha nos bastidores em certos casos).
  • Como não há tipos de referência agregados, não há passagem de pilha dos tipos de referência agregados. (Pense na passagem eficiente de argumentos de vetor)
  • A coleta de lixo pode prejudicar o conteúdo do cache L1 / L2 e as pausas de parada mundial do GC prejudicam a interatividade.
  • A conversão entre tipos de dados sempre requer cópia; você não pode levar um ponteiro para um monte de bytes que você obteve de um soquete e interpretá-los como um flutuador.

Algumas dessas coisas são compensações (não ter que fazer o gerenciamento manual de memória vale a pena dar muito desempenho à maioria das pessoas), algumas são provavelmente o resultado de tentar manter o Java simples e outras são erros de design (embora possivelmente apenas em retrospectiva) , ou seja, UTF-16 era uma codificação de comprimento fixo quando o Java foi criado, o que torna a decisão de escolhê-lo muito mais compreensível).

Vale a pena notar que muitas dessas compensações são muito diferentes para Java / JVM e para C # / CIL. O .NET CIL possui estruturas do tipo referência, alocação / passagem de pilha, matrizes compactadas de estruturas e genéricos instanciados por tipo.


37
+1 - no geral, esta é uma boa resposta. No entanto, não tenho certeza de que o marcador "não haja alocação de pilha" seja totalmente preciso. As JITs Java frequentemente escapam da análise para permitir a alocação de pilha sempre que possível - talvez o que você deva dizer é que a linguagem Java não permite que o programador decida quando um objeto é alocado por pilha versus alocado por pilha. Além disso, se um coletor de lixo geracional (que todas as JVMs modernas usam) estiver em uso, "alocação de heap" significa algo completamente diferente (com características de desempenho completamente diferentes) do que em um ambiente C ++.
Daniel Pryden #

5
Eu acho que existem duas outras coisas, mas eu principalmente trabalho com coisas em um nível muito mais alto, então diga se estou errado. Você realmente não pode escrever C ++ sem desenvolver um conhecimento mais geral do que realmente está acontecendo na memória e como o código da máquina realmente funciona, enquanto as linguagens de script ou de máquina virtual abstraem todas essas coisas da sua atenção. Você também tem muito mais controle refinado sobre como as coisas funcionam, enquanto em uma VM ou linguagem interpretada você conta com o que os autores principais da biblioteca podem ter otimizado para um cenário excessivamente específico.
Erik Reppen

18
+1. Mais uma coisa que eu acrescentaria (mas não estou disposto a enviar uma nova resposta): a indexação de array em Java sempre envolve verificação de limites. Com C e C ++, este não é o caso.
Rjalk3

7
Vale a pena notar que a alocação de heap do Java é significativamente mais rápida que uma versão ingênua com C ++ (devido ao pool interno e outras coisas), mas a alocação de memória em C ++ pode ser significativamente melhor se você souber o que está fazendo.
Brendan Long

10
@BrendanLong, true .. mas apenas se a memória estiver limpa - uma vez que um aplicativo esteja em execução por um tempo, a alocação de memória será mais lenta devido à necessidade do GC, que diminui drasticamente as coisas, pois libera memória, executa finalizadores e, em seguida, compactar. É uma troca que beneficia benchmarks, mas (IMHO), em geral, diminui os aplicativos.
Gbjbaanb

67

É simplesmente porque o C ++ é compilado no código de montagem / máquina, enquanto o Java / C # ainda tem a sobrecarga de processamento da compilação JIT em tempo de execução?

Parcialmente, mas em geral, assumindo um compilador JIT de ponta absolutamente fantástico, o código C ++ adequado ainda tende a ter um desempenho melhor do que o código Java por duas razões principais:

1) Os modelos C ++ fornecem melhores recursos para escrever código genérico e eficiente . Os modelos fornecem ao programador C ++ uma abstração muito útil que possui sobrecarga de tempo de execução ZERO. (Os modelos são basicamente digitação em tempo de compilação.) Por outro lado, o melhor que você obtém com os genéricos Java é basicamente funções virtuais. As funções virtuais sempre têm uma sobrecarga de tempo de execução e geralmente não podem ser incorporadas.

Em geral, a maioria das linguagens, incluindo Java, C # e até C, permite escolher entre eficiência e generalidade / abstração. Os modelos C ++ oferecem os dois (ao custo de tempos de compilação mais longos).

2) O fato de o padrão C ++ não ter muito a dizer sobre o layout binário de um programa C ++ compilado oferece aos compiladores C ++ muito mais liberdade do que um compilador Java, permitindo otimizações melhores (às vezes com mais dificuldades na depuração). ) De fato, a própria natureza da especificação da linguagem Java impõe uma penalidade de desempenho em determinadas áreas. Por exemplo, você não pode ter uma matriz contígua de objetos em Java. Você só pode ter uma matriz contígua de ponteiros de objeto(referências), o que significa que a iteração sobre uma matriz em Java sempre incorre no custo da indireção. A semântica de valores do C ++, no entanto, habilita matrizes contíguas. Outra diferença é o fato de o C ++ permitir que objetos sejam alocados na pilha, enquanto Java não, o que significa que, na prática, como a maioria dos programas em C ++ costuma alocar objetos na pilha, o custo da alocação geralmente é próximo de zero.

Uma área em que o C ++ pode ficar atrás do Java é qualquer situação em que muitos objetos pequenos precisem ser alocados no heap. Nesse caso, o sistema de coleta de lixo do Java provavelmente resultará em melhor desempenho do que o padrão newe deleteno C ++ porque o Java GC permite a desalocação em massa. Porém, novamente, um programador C ++ pode compensar isso usando um pool de memória ou alocador de laje, enquanto um programador Java não tem recurso quando se depara com um padrão de alocação de memória para o qual o tempo de execução Java não é otimizado.

Além disso, consulte esta excelente resposta para obter mais informações sobre este tópico.


6
Boa resposta, mas um ponto menor: "Os modelos C ++ oferecem os dois (ao custo de tempos de compilação mais longos.)" Eu acrescentaria também o custo do tamanho maior do programa. Nem sempre pode ser um problema, mas se estiver desenvolvendo para dispositivos móveis, definitivamente pode ser.
Leo

9
@luiscubal: não, nesse aspecto, os genéricos de C # são muito parecidos com Java (em que o mesmo caminho de código "genérico" é usado, independentemente de quais tipos são passados.) O truque para os modelos de C ++ é que o código é instanciado uma vez por todo tipo ao qual é aplicado. O mesmo std::vector<int>ocorre com uma matriz dinâmica projetada apenas para ints, e o compilador é capaz de otimizá-la adequadamente. AC # List<int>ainda é apenas um List.
jalf

12
@jalf C # List<int>usa um int[], e não um Object[]como Java faz. Veja stackoverflow.com/questions/116988/…
luiscubal

5
@luiscubal: sua terminologia não é clara. O JIT não age no que eu consideraria "tempo de compilação". Você está certo, é claro, dado um compilador JIT suficientemente inteligente e agressivo, efetivamente não há limites para o que ele poderia fazer. Mas o C ++ requer esse comportamento. Além disso, os modelos C ++ permitem ao programador especificar especializações explícitas, permitindo otimizações explícitas adicionais, quando aplicável. C # não tem equivalente para isso. Por exemplo, em C ++, poderia definir um vector<N>em que, para o caso específico de vector<4>, minha implementação SIMD-codificada mão deve ser usado
jalf

5
@ Leo: Código inchar através de modelos foi um problema há 15 anos. Com modelagem e inlining pesados, além de compiladores de habilidades escolhidos desde (como dobrar instâncias idênticas), muito código fica menor nos modelos hoje em dia.
S7

46

O que as outras respostas (6 até agora) parecem ter esquecido de mencionar, mas o que considero muito importante para responder a isso é uma das filosofias de design muito básicas do C ++, formuladas e empregadas pela Stroustrup desde o dia 1:

Você não paga pelo que não usa.

Existem alguns outros princípios importantes de design subjacentes que moldaram muito o C ++ (assim você não deve ser forçado a adotar um paradigma específico), mas você não paga pelo que não usa , entre os mais importantes.


Em seu livro The Design and Evolution of C ++ (geralmente chamado de [D&E]), Stroustrup descreve que necessidade ele tinha que o fez criar C ++ em primeiro lugar. Nas minhas próprias palavras: Para sua tese de doutorado (algo a ver com simulações de rede, IIRC), ele implementou um sistema no SIMULA, que ele gostou muito, porque a linguagem era muito boa para permitir que ele expressasse seus pensamentos diretamente no código. No entanto, o programa resultante ficou muito lento e, para se formar, ele reescreveu a coisa em BCPL, um predecessor de C. Escrevendo o código em BCPL que ele descreve como uma dor, mas o programa resultante foi rápido o suficiente para entregar resultados, o que lhe permitiu terminar o doutorado.

Depois disso, ele queria uma linguagem que permitisse traduzir problemas do mundo real em código o mais diretamente possível, mas também permitisse que o código fosse muito eficiente.
Em busca disso, ele criou o que mais tarde se tornaria C ++.


Portanto, a meta citada acima não é apenas um dos vários princípios fundamentais de design subjacentes; está muito próxima da razão de ser do C ++. E pode ser encontrado em praticamente qualquer lugar do idioma: as funções são apenas virtualquando você deseja (porque a chamada de funções virtuais vem com uma pequena sobrecarga) Os PODs são inicializados apenas automaticamente quando você solicita isso explicitamente; as exceções só custam desempenho quando você realmente jogá-los (considerando que era um objetivo explícito do projeto permitir que a configuração / limpeza de quadros de pilha fosse muito barata), nenhum GC sendo executado sempre que lhe parecer, etc.

O C ++ optou explicitamente por não fornecer algumas conveniências ("eu tenho que tornar esse método virtual aqui?") Em troca de desempenho ("não, não tenho, e agora o compilador pode inlinee otimiza o heck-out do coisa toda! ") e, não surpreendentemente, isso realmente resultou em ganhos de desempenho em comparação aos idiomas mais convenientes.


4
Você não paga pelo que não usa. => e eles adicionaram RTTI :(
Matthieu M.

11
@ Matthieu: Enquanto eu entendo o seu sentimento, não posso deixar de notar que mesmo isso foi adicionado com cuidado em relação ao desempenho. O RTTI é especificado para que possa ser implementado usando tabelas virtuais e, portanto, adiciona muito pouca sobrecarga se você não o usar. Se você não usa polimorfismo, não há custo algum. Estou esquecendo de algo?
SBI

9
@ Matthieu: Claro, há razão. Mas essa razão é racional? Pelo que pude ver, o "custo do RTTI", se não usado, é um ponteiro adicional na tabela virtual de todas as classes polimórficas, apontando para algum objeto RTTI alocado estaticamente em algum lugar. A menos que você queira programar o chip na minha torradeira, como isso pode ser relevante?
SBI

4
@Aaronaught: Estou sem saber o que responder a isso. Você realmente acabou de rejeitar minha resposta porque ela aponta a filosofia subjacente que levou Stroustrup et al a adicionar recursos de uma maneira que permita desempenho, em vez de listar essas maneiras e recursos individualmente?
SBI

9
@Aaronaught: Você tem minha simpatia.
S8

29

Você conhece o trabalho de pesquisa do Google sobre esse tópico?

Da conclusão:

Descobrimos que em relação ao desempenho, o C ++ vence por uma grande margem. No entanto, também exigiu os mais amplos esforços de ajuste, muitos dos quais foram feitos em um nível de sofisticação que não estaria disponível para o programador médio.

Essa é pelo menos uma explicação parcial, no sentido de "porque os compiladores C ++ do mundo real produzem código mais rápido que os compiladores Java por medidas empíricas".


4
Além das diferenças de uso de memória e cache, uma das mais importantes é a quantidade de otimização realizada. Compare quantas otimizações o GCC / LLVM (e provavelmente o Visual C ++ / ICC) fazem em relação ao compilador Java HotSpot: muito mais, especialmente em relação a loops, eliminando ramificações redundantes e alocação de registros. Os compiladores JIT geralmente não têm tempo para essas otimizações agressivas, mesmo pensando que poderiam implementá-las melhor usando as informações de tempo de execução disponíveis.
Gratian Lup

2
@GratianLup: Gostaria de saber se isso ainda é verdade no LTO.
Deduplicator

2
@GratianLup: Não vamos esquecer otimização guiada por perfil para C ++ ...
Deduplicator

23

Esta não é uma duplicata das suas perguntas, mas a resposta aceita responde à maior parte da sua pergunta: Uma revisão moderna do Java

Resumindo:

Fundamentalmente, a semântica do Java determina que é uma linguagem mais lenta que o C ++.

Portanto, dependendo de qual outro idioma você comparar C ++, você poderá obter ou não a mesma resposta.

Em C ++ você tem:

  • Capacidade para fazer inlining inteligente,
  • geração de código genérico com forte localidade (modelos)
  • dados tão pequenos e compactos quanto possível
  • oportunidades para evitar indireções
  • comportamento previsível da memória
  • otimizações do compilador possíveis apenas devido ao uso de abstrações de alto nível (modelos)

Esses são os recursos ou efeitos colaterais da definição de idioma que o torna teoricamente mais eficiente em memória e velocidade do que qualquer idioma que:

  • use indiretamente massivamente (linguagens "tudo é uma referência / ponteiro gerenciado"): indirection significa que a CPU precisa pular na memória para obter os dados necessários, aumentando as falhas de cache da CPU, o que significa retardar o processamento - C também usa indiretas. muito mesmo que possa ter pequenos dados como C ++;
  • gerar objetos de tamanho grande aos quais os membros são acessados ​​indiretamente: isso é uma conseqüência de ter referências por padrão; os membros são ponteiros; portanto, quando você obtém um membro, pode não obter dados próximos ao núcleo do objeto pai, desencadeando novamente falhas no cache.
  • use um coletor de garbarge: apenas impossibilita a previsibilidade do desempenho (por design).

O inline agressivo em C ++ do compilador reduz ou elimina muitos indirecionamentos. A capacidade de gerar um pequeno conjunto de dados compactos facilita o armazenamento em cache se você não espalhar esses dados por toda a memória, em vez de agrupá-los (ambos são possíveis, o C ++ permite que você escolha). O RAII torna previsível o comportamento da memória C ++, eliminando muitos problemas no caso de simulações em tempo real ou semi-em tempo real, que exigem alta velocidade. Os problemas de localização, em geral, podem ser resumidos por isso: quanto menor o programa / dados, mais rápida é a execução. O C ++ fornece diversas maneiras de garantir que seus dados estejam onde você deseja que estejam (em um pool, em uma matriz ou o que seja) e que sejam compactos.

Obviamente, existem outras linguagens que podem fazer o mesmo, mas são apenas menos populares porque não fornecem tantas ferramentas de abstração quanto o C ++, portanto, são menos úteis em muitos casos.


7

É principalmente sobre memória (como disse Michael Borgwardt) com um pouco de ineficiência do JIT adicionada.

Uma coisa não mencionada é o cache - para usá-lo completamente, você precisa que seus dados sejam dispostos de forma contígua (ou seja, todos juntos). Agora, com um sistema de GC, a memória é alocada no heap do GC, o que é rápido, mas à medida que a memória é usada, o GC entra em ação regularmente e remove os blocos que não são mais usados ​​e depois compacta o restante. Agora, além da lentidão óbvia de juntar os blocos usados, isso significa que os dados que você está usando podem não estar juntos. Se você tiver uma matriz de 1.000 elementos, a menos que você os aloque de uma só vez (e atualize o conteúdo deles em vez de excluir e criar novos - que serão criados no final do heap), eles serão espalhados por todo o heap, exigindo, portanto, várias ocorrências de memória para lê-las no cache da CPU. O aplicativo AC / C ++ provavelmente alocará a memória para esses elementos e você atualizará os blocos com os dados. (ok, existem estruturas de dados como uma lista que se comportam mais como as alocações de memória do GC, mas as pessoas sabem que são mais lentas que os vetores).

Você pode ver isso em operação simplesmente substituindo qualquer objeto StringBuilder por String ... Os construtores de string funcionam pré-alocando memória e preenchendo-a, e é um truque de desempenho conhecido para sistemas java / .NET.

Não se esqueça de que o paradigma 'excluir antigas e alocar novas cópias' é muito usado em Java / C #, simplesmente porque as pessoas dizem que as alocações de memória são realmente rápidas devido ao GC e, portanto, o modelo de memória dispersa é usado em qualquer lugar ( exceto para construtores de strings, é claro), portanto, todas as suas bibliotecas tendem a desperdiçar memória e a usar muito, nenhuma delas obtém o benefício da contiguidade. Culpe o hype em torno da GC por isso - eles disseram que a memória estava livre, lol.

O GC em si é obviamente outro sucesso - quando é executado, ele não só precisa varrer o heap, mas também liberar todos os blocos não utilizados e, em seguida, executar os finalizadores (embora isso fosse feito separadamente da próxima vez que o aplicativo for interrompido) (não sei se ainda é um sucesso tão bom, mas todos os documentos que li dizem que usam apenas finalizadores se realmente necessário) e, em seguida, ele deve mover esses blocos para a posição correta compactado e atualize a referência para o novo local do bloco. Como você pode ver, é muito trabalho!

As ocorrências de perf para a memória C ++ se resumem às alocações de memória - quando você precisa de um novo bloco, precisa percorrer o heap procurando o próximo espaço livre que seja grande o suficiente, com um heap fortemente fragmentado, que não é tão rápido quanto o de um GC 'apenas aloque outro bloco no final', mas acho que não é tão lento quanto todo o trabalho que a compactação do GC faz e pode ser mitigado usando vários heaps de bloco de tamanho fixo (também conhecidos como pools de memória).

Há mais ... como carregar assemblies fora do GAC que exigem verificação de segurança, caminhos de sondagem (ative o sxstrace e apenas veja o que está fazendo!) E outra engenharia em geral que parece ser muito mais popular com java / .net que C / C ++.


2
Muitas coisas que você escreve não são verdadeiras para os coletores de lixo geracionais modernos.
22612 Michael Borgwardt

3
@MichaelBorgwardt como? Eu digo "o GC é executado regularmente" e "compacta a pilha". O restante da minha resposta diz respeito a como as estruturas de dados do aplicativo usam a memória.
Gbjbaanb

6

"É simplesmente porque o C ++ é compilado no código de montagem / máquina, enquanto o Java / C # ainda tem a sobrecarga de processamento da compilação JIT em tempo de execução?" Basicamente, sim!

Nota rápida, porém, o Java tem mais despesas gerais do que apenas a compilação JIT. Por exemplo, ele faz muito mais verificação para você (que é como faz coisas como ArrayIndexOutOfBoundsExceptionse NullPointerExceptions). O coletor de lixo é outra sobrecarga significativa.

Há uma comparação bem detalhada aqui .


2

Lembre-se de que o seguinte é apenas comparando a diferença entre compilação nativa e JIT e não cobre as especificidades de nenhum idioma ou estrutura em particular. Pode haver razões legítimas para escolher uma plataforma específica além disso.

Quando afirmamos que o código nativo é mais rápido, estamos falando do caso de uso típico de código compilado nativamente versus código compilado JIT, em que o uso típico de um aplicativo compilado JIT deve ser executado pelo usuário, com resultados imediatos (por exemplo, não esperando primeiro no compilador). Nesse caso, não acho que alguém possa afirmar com uma cara séria que o código compilado JIT pode corresponder ou vencer o código nativo.

Vamos supor que temos um programa escrito em alguma linguagem X, e podemos compilá-lo com um compilador nativo e novamente com um compilador JIT. Cada fluxo de trabalho possui os mesmos estágios envolvidos, que podem ser generalizados como (Código -> Representação Intermediária -> Código da Máquina -> Execução). A grande diferença entre dois é que etapas são vistas pelo usuário e quais são vistas pelo programador. Com a compilação nativa, o programador vê tudo, exceto o estágio de execução, mas com a solução JIT, a compilação no código da máquina é vista pelo usuário, além da execução.

A afirmação de que A é mais rápido que B refere-se ao tempo necessário para a execução do programa, conforme visto pelo usuário . Se assumirmos que os dois trechos de código executam identicamente no estágio Execution, devemos assumir que o fluxo de trabalho JIT é mais lento para o usuário, pois ele também deve ver o tempo T da compilação para o código da máquina, em que T> 0. Então , para qualquer possibilidade de o fluxo de trabalho JIT executar o mesmo que o fluxo de trabalho nativo, para o usuário, devemos diminuir o tempo de Execução do código, de forma que Execução + Compilação para código de máquina seja menor do que apenas o estágio Execution do fluxo de trabalho nativo. Isso significa que devemos otimizar o código melhor na compilação JIT do que na compilação nativa.

Isso, no entanto, é bastante inviável, já que para realizar as otimizações necessárias para acelerar a Execução, precisamos gastar mais tempo na fase de compilação para o código da máquina e, assim, qualquer tempo que salvarmos como resultado do código otimizado será realmente perdido, pois nós o adicionamos à compilação. Em outras palavras, a "lentidão" de uma solução baseada em JIT não é meramente devido ao tempo adicional para a compilação JIT, mas o código produzido por essa compilação é mais lento que uma solução nativa.

Vou usar um exemplo: Registrar alocação. Como o acesso à memória é milhares de vezes mais lento que o acesso ao registro, idealmente, queremos usar registros sempre que possível e ter o mínimo de acesso possível, mas temos um número limitado de registros e precisamos derramar estado na memória quando precisarmos. um registro. Se usarmos um algoritmo de alocação de registro que leva 200ms para calcular e, como resultado, economizamos 2ms em tempo de execução - não estamos fazendo o melhor uso possível para um compilador JIT. Soluções como o algoritmo de Chaitin, que pode produzir código altamente otimizado, são inadequadas.

O papel do compilador JIT é encontrar o melhor equilíbrio entre o tempo de compilação e a qualidade do código produzido, no entanto, com um grande viés no tempo de compilação rápido, pois você não deseja deixar o usuário esperando. O desempenho do código que está sendo executado é mais lento no caso JIT, pois o compilador nativo não fica muito vinculado (otimizado) pelo tempo na otimização do código, portanto, é livre para usar os melhores algoritmos. A possibilidade de que a compilação + execução geral para um compilador JIT possa superar apenas o tempo de execução do código compilado nativamente é efetivamente 0.

Mas nossas VMs não se limitam apenas à compilação JIT. Eles empregam técnicas de compilação antecipadas, armazenamento em cache, hot swap e otimizações adaptativas. Então, vamos modificar nossa alegação de que o desempenho é o que o usuário vê e limitar o tempo necessário para a execução do programa (suponha que tenhamos compilado AOT). Podemos efetivamente tornar o código em execução equivalente ao compilador nativo (ou talvez melhor?). Uma grande reivindicação das VMs é que elas podem produzir código de melhor qualidade do que um compilador nativo, porque ele tem acesso a mais informações - a do processo em execução, como a frequência com que uma determinada função pode ser executada. A VM pode aplicar otimizações adaptáveis ​​ao código mais essencial via hot swap.

No entanto, existe um problema com esse argumento - ele pressupõe que a otimização guiada por perfil e similares é algo exclusivo das VMs, o que não é verdade. Também podemos aplicá-lo à compilação nativa - compilando nosso aplicativo com o perfil ativado, registrando as informações e recompilando o aplicativo com esse perfil. Provavelmente, também vale a pena ressaltar que a troca a quente de código não é algo que apenas um compilador JIT pode fazer, podemos fazê-lo para código nativo - embora as soluções baseadas em JIT para fazer isso estejam mais prontamente disponíveis e muito mais fáceis para o desenvolvedor. Portanto, a grande questão é: uma VM pode nos oferecer algumas informações que a compilação nativa não pode, o que pode aumentar o desempenho do nosso código?

Eu não posso ver isso sozinho. Também podemos aplicar a maioria das técnicas de uma VM típica ao código nativo - embora o processo esteja mais envolvido. Da mesma forma, podemos aplicar qualquer otimização de um compilador nativo de volta a uma VM que usa compilação AOT ou otimizações adaptativas. A realidade é que a diferença entre código executado nativamente e executado em uma VM não é tão grande quanto acreditamos. No final, eles levam ao mesmo resultado, mas adotam uma abordagem diferente para chegar lá. A VM usa uma abordagem iterativa para produzir código otimizado, onde o compilador nativo espera isso desde o início (e pode ser aprimorado com uma abordagem iterativa).

Um programador de C ++ pode argumentar que ele precisa das otimizações desde o início e não deve esperar uma VM descobrir como fazê-las, se houver. Este é provavelmente um ponto válido com a nossa tecnologia atual, pois o nível atual de otimizações em nossas VMs é inferior ao que os compiladores nativos podem oferecer - mas isso nem sempre pode ser o caso se as soluções AOT em nossas VMs melhorarem etc.


0

Este artigo é um resumo de um conjunto de postagens de blog tentando comparar a velocidade de c ++ vs c # e os problemas que você precisa superar nos dois idiomas para obter código de alto desempenho. O resumo é 'sua biblioteca importa muito mais do que qualquer coisa, mas se você estiver em c ++, poderá superar isso'. ou 'linguagens modernas têm melhores bibliotecas e, portanto, obtêm resultados mais rápidos com menor esforço', dependendo de sua inclinação filosófica.


0

Eu acho que a verdadeira questão aqui não é "o que é mais rápido?" mas "qual tem o melhor potencial para obter melhor desempenho?". Visto nesses termos, o C ++ vence claramente - ele é compilado no código nativo, não há JITting, é um nível mais baixo de abstração etc.

Isso está longe da história completa.

Como o C ++ é compilado, qualquer otimização do compilador deve ser feita no momento da compilação, e as otimizações do compilador apropriadas para uma máquina podem estar completamente erradas para outra. Também é possível que qualquer otimização global do compilador possa favorecer certos algoritmos ou padrões de código em detrimento de outros.

Por outro lado, um programa JITted otimizará no momento do JIT, para que ele possa fazer alguns truques que um programa pré-compilado não pode e pode fazer otimizações muito específicas para a máquina na qual está sendo executado e o código que está sendo executado. Depois de superar a sobrecarga inicial do JIT, em alguns casos, é possível que seja mais rápido.

Em ambos os casos, uma implementação sensata do algoritmo e outras instâncias do programador não sendo estúpido provavelmente serão fatores muito mais significativos; no entanto - por exemplo, é perfeitamente possível escrever um código de string completamente inoperante em C ++ que será bloqueado até uma linguagem de script interpretada.


3
"otimizações do compilador que são apropriadas para uma máquina podem estar completamente erradas para outra" Bem, isso não é realmente a culpa do idioma. Um código realmente crítico para o desempenho pode ser compilado separadamente para cada máquina em que será executado, o que é um acéfalo se você compilar localmente a partir da fonte ( -march=native). - "é um nível mais baixo de abstração" não é realmente verdade. O C ++ usa abstrações de nível tão alto quanto Java (ou, de fato, mais altas: programação funcional? Metaprogramação de modelos?), Apenas implementa as abstrações de maneira menos "limpa" do que Java.
leftaroundabout

"Um código realmente crítico para o desempenho pode ser compilado separadamente para cada máquina na qual ele será executado, o que é um acéfalo se você compilar localmente a partir da fonte" - isso falha devido a uma suposição subjacente de que o usuário final também é um programador.
Maximus Minimus

Não necessariamente o usuário final, apenas a pessoa responsável pela instalação do programa. Nos computadores e dispositivos móveis, esse normalmente é o usuário final, mas esses não são os únicos aplicativos que existem, certamente não são os mais críticos para o desempenho. E você realmente não precisa ser um programador para criar um programa a partir do código-fonte, se ele tiver scripts de construção escritos corretamente, como todos os bons projetos de software livre / aberto.
precisa saber é o seguinte

1
Enquanto na teoria sim, um JIT pode fazer mais truques do que um compilador estático, na prática (pelo menos para .NET, eu também não conheço java), ele não faz nada disso. Recentemente, fiz uma desmontagem do código .NET JIT do .NET, e há todo tipo de otimização, como extrair código de loops, eliminação de código morto etc., que o .NET JIT simplesmente não realiza. Eu gostaria que o faria, mas hey, a equipe do Windows dentro da Microsoft vem tentando matar .NET durante anos, então eu não estou prendendo a respiração
Orion Edwards

-1

A compilação JIT realmente tem um impacto negativo no desempenho. Se você projetar um compilador "perfeito" e um compilador JIT "perfeito", a primeira opção sempre terá desempenho.

Java e C # são interpretados em linguagens intermediárias e, em seguida, compilados em código nativo em tempo de execução, o que reduz o desempenho.

Mas agora a diferença não é tão óbvia para C #: o Microsoft CLR produz código nativo diferente para diferentes CPUs, tornando o código mais eficiente para a máquina em execução, o que nem sempre é feito pelos compiladores C ++.

O PS C # é escrito com muita eficiência e não possui muitas camadas de abstração. Isso não é verdade para Java, que não é tão eficiente. Portanto, nesse caso, com seu CLR greate, os programas C # geralmente apresentam melhor desempenho que os programas C ++. Para saber mais sobre .Net e CLR, consulte o "CLR via C #" de Jeffrey Richter .


8
Se o JIT realmente tivesse um impacto negativo no desempenho, certamente não seria usado?
Zavior

2
@ Zavior - Não consigo encontrar uma boa resposta para sua pergunta, mas não vejo como o JIT não pode adicionar uma sobrecarga extra de desempenho - o JIT é um processo extra a ser concluído em tempo de execução que requer recursos que não são ' sendo gasto na execução do próprio programa, enquanto uma linguagem totalmente compilada está 'pronta para uso'.
Anónimo

3
O JIT tem um efeito positivo no desempenho, e não negativo, se você o contextualizar - ele está compilando o código de bytes no código da máquina antes de executá-lo. Os resultados também podem ser armazenados em cache, permitindo a execução mais rápida que o código de bytes equivalente que é interpretado.
Casey Kuball

3
O JIT (ou melhor, a abordagem de bytecode) não é usado para desempenho, mas por conveniência. Em vez de pré-construir binários para cada plataforma (ou um subconjunto comum, que é subótimo para cada uma delas), você compila apenas até a metade e deixa o compilador JIT fazer o resto. 'Escreva uma vez, implante em qualquer lugar' é por isso que é feito dessa maneira. A conveniência pode ser tido com apenas um interpretador bytecode, mas JIT faz com que seja mais rápido do que o intérprete raw (embora não necessariamente rápido o suficiente para bater uma solução pré-compilado; compilação JIT faz exame do tempo, eo resultado nem sempre faz-se por isso).
tdammers

4
@ Tdammmers, na verdade também há um componente de desempenho. Consulte java.sun.com/products/hotspot/whitepaper.html . As otimizações podem incluir itens como ajustes dinâmicos para melhorar a previsão de ramificação e acertos de cache, inlining dinâmico, des virtualização, desativação da verificação de limites e desenrolamento de loop. A alegação é que, em muitos casos, esses custos podem mais do que pagar pelo custo do JIT.
Charles E. Grant
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.