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.
fonte
Respostas:
É 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:
Alguns outros fatores relacionados à memória, mas não ao cache:
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.
fonte
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
new
edelete
no 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.
fonte
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 umList
.List<int>
usa umint[]
, e não umObject[]
como Java faz. Veja stackoverflow.com/questions/116988/…vector<N>
em que, para o caso específico devector<4>
, minha implementação SIMD-codificada mão deve ser usadoO 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
virtual
quando 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
inline
e 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.fonte
Você conhece o trabalho de pesquisa do Google sobre esse tópico?
Da conclusão:
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".
fonte
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:
Portanto, dependendo de qual outro idioma você comparar C ++, você poderá obter ou não a mesma resposta.
Em C ++ você tem:
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:
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.
fonte
É 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 ++.
fonte
"É 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
ArrayIndexOutOfBoundsExceptions
eNullPointerExceptions
). O coletor de lixo é outra sobrecarga significativa.Há uma comparação bem detalhada aqui .
fonte
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.
fonte
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.
fonte
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.
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.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 .
fonte