Executando verificação de segurança...
6
Carregando publicação patrocinada...
3
1

Não sei porque ficou mais lento. De novo AOT não consegue fazer milagres. Não testei com PGO, mas mesmo assim acredito que ficaria mais lento. Assim que tiver um tempo vou testar GraalVM com PGO.

$ native-image --gc=G1 -R:+AlwaysPreTouch -R:InitialHeapSize=4g -R:MaxHeapSize=4g \
               -R:InitialHeapSize=512m -R:MaxHeapSize=1024m -march=native \
               -cp target/coralbench-all.jar com.coralblocks.coralbench.example.IntMapBenchmark \
               -o target/graal/IntMapBenchmark --no-fallback -O3 --initialize-at-build-time

$ ./target/graal/IntMapBenchmark 0 10000000 5000000

Arguments: warmup=0 measurements=10000000 mapCapacity=5000000

Benchmarking put...
Measurements: 10,000,000 | Warm-Up: 0 | Iterations: 10,000,000
Avg Time: 41.110 nanos | Min Time: 25.000 nanos | Max Time: 21.841 millis
75% = [avg: 29.000 nanos, max: 31.000 nanos]
90% = [avg: 29.000 nanos, max: 32.000 nanos]
99% = [avg: 30.000 nanos, max: 41.000 nanos]
99.9% = [avg: 30.000 nanos, max: 100.000 nanos]
99.99% = [avg: 30.000 nanos, max: 339.000 nanos]
99.999% = [avg: 30.000 nanos, max: 1.573 micros]

Benchmarking get...
Measurements: 10,000,000 | Warm-Up: 0 | Iterations: 10,000,000
Avg Time: 24.560 nanos | Min Time: 19.000 nanos | Max Time: 15.679 micros
75% = [avg: 22.000 nanos, max: 25.000 nanos]
90% = [avg: 23.000 nanos, max: 26.000 nanos]
99% = [avg: 23.000 nanos, max: 39.000 nanos]
99.9% = [avg: 24.000 nanos, max: 97.000 nanos]
99.99% = [avg: 24.000 nanos, max: 382.000 nanos]
99.999% = [avg: 24.000 nanos, max: 550.000 nanos]

Benchmarking remove...
Measurements: 10,000,000 | Warm-Up: 0 | Iterations: 10,000,000
Avg Time: 35.800 nanos | Min Time: 23.000 nanos | Max Time: 165.095 micros
75% = [avg: 30.000 nanos, max: 33.000 nanos]
90% = [avg: 30.000 nanos, max: 36.000 nanos]
99% = [avg: 31.000 nanos, max: 93.000 nanos]
99.9% = [avg: 32.000 nanos, max: 123.000 nanos]
99.99% = [avg: 32.000 nanos, max: 457.000 nanos]
99.999% = [avg: 34.000 nanos, max: 63.089 micros]
3

Bom, nesse caso não há muito o que uma linguagem de programação AOT possa fazer para competir com linguagens que usa JIT. Mas há casos e casos:

  1. Se seu programa precisa ser leve com pouco gasto de RAM e Memória: AOT
  2. Se seu programa for criado para iniciar super rápido e ser usado rapidamente: AOT

Em outros casos, não há problema nenhum em usar JIT sabendo que:

  1. Apesar do aumento de performance, o JIT vai consumir mais cpu e memória para otimizar o seu código em execução

  2. A inicialização de um processo com JIT é mais lento que a inicialização de um projeto AOT por conter estruturas adicionais para compilação in-runtime do projeto

Mas se seu código vai ficar por muito tempo vivo, de preferência continuamente executando alguma coisa então não tem nada de errado em usar JIT.

Inclusive o canal do Andreas Kling que eu citei em outro post, para lidar com o ladybird, seu webbrowser ele decidiu criar um interpretador javascript próprio chamado LibJS que contém um JIT próprio e você pode acompanhar por exemplo este Vídeo sobre ele prototipando um JIT

E voltando ao assunto principal, não tem com uma linguagem AOT competir com um JIT porque as abordagens são diferentes:

AOT: Passei uma vez pelo código e o binário vai ser justamente isso aqui x.
JIT: Tive que passar várias vezes pelo mesmo trecho de código então acho que posso otimizar isso aqui x.

2

AOT: Passei uma vez pelo código e o binário vai ser justamente isso aqui x.
JIT: Tive que passar várias vezes pelo mesmo trecho de código então acho que posso otimizar isso aqui x.

Valeu pelas dicas, vou assistir. Eu vejo o trade-off assim:

AOT: Não vai perder tempo durante o run-time compilando nada. Não precisa esquentar (warmup).
JIT: Vai perder tempo durante o run-time compilando os hot spots. Vai precisar de um tempo de warmup para peak performance.

Só que o JIT pode fazer otimizações muito mais agressivas que o AOT. Acho que o pessoal sempre achou que AOT com -O3 era a coisa mais rápida do mundo. Pelo testes que eu venho fazendo aí parece que perde muitas vezes para o JIT. Runtime information ajuda muito nas otimizações mais agressivas, principalmente no inlining. Sem saber os hotspots, o AOT vai acabar deixando de fazer inlining nas coisas mais importantes. Uma idéia que pensei aqui: por que os compiiladores de C++ não possuem alguma diretriz para forçar inlining de algum método no matter what? Ou tem isso e eu desconheço?

O que se faz é usar PGO (profiling information) that can be fed into the AOT compiler. Tanto o AOT quanto o JIT podem ter isso. Mas daí o JIT leva muita vantagem, porque se os HotSpots mudarem em tempo de execução ele pode se re-adaptar, ou seja, recompilar os novos hot spots. AOT não existe depois que o código começa a executar.

Na minha cabeça (posso estar errado) o problema é que se o código muda, daí vc tem que refazer o PGO para refletir o novo código. Se o código muda pouco no problem. Se muda muito aí complica.

Agora para concluir, minhas pesquisas com o PGO do GraalVM e da ZingVM (ReadyNow) mostraram uma melhorar de no máximo 50%. Ou seja, não chega perto do JIT em tempo de execução não. É uma ajuda, mas não é a solução para não precisar de JIT.

NOTE: PGO = Profile-guided optimizations

2

por que os compiiladores de C++ não possuem alguma diretriz para forçar inlining de algum método no matter what?

Forçar inline indiscriminadamente é impacta no cache de instruções e o aumento do tamanho do binário, pode acabar poluindo o cache de instruções (https://isocpp.org/wiki/faq/inline-functions#inline-and-perf).

Sobre os hotspots mudarem durante a execução, isso não costuma ser uma preocupação no contexto de C++, porque ela é geralmente escolhida para cenários onde sabemos que estamos lidando com rotinas CPU-bound e grandes volumes de dados. Nesses cenários, mesmo que surjam novas features, times experientes em C++ geralmente já estão espertos para identificar isso.

2

Forçar inline indiscriminadamente é impacta no cache de instruções e o aumento do tamanho do binário, pode acabar poluindo o cache de instruções

Sim, claro. Não estava sugerindo fazer inline de tudo. Isso obviamente é impraticável e é exatamente por isso que o JIT acaba levando vantagem. Minha sugestão foi:

O AOT não tem como saber os hot spots. O JIT tem, mas gasta tempo de runtime para descborir. Já o programador possui esse conhecimento, até melhor que o AOT e o JIT. Então ele (o programador) pegaria aqueles métodos que ele sabe que são quentes (hot) e anotaria eles com um @Inline para forçar que esses métodos, independente do tamanho deles, fossem inlined.

Por que o compilador de C++ ou Java não oferecem isso?

2

Vi essa discussão há uns 20 anos atrás, e é curioso como esses assuntos acabam voltando. A conclusão da época foi: código manso por código manso, JIT quase sempre se sai melhor. O ponto é que escolhemos C/C++ quando cabe no projeto explorar mais truques do chapeu para otimizacão, ou seja código brabo). Os truques incluem metaprogramação de templates, criação de memory pools, paralelização, incluir códigos em assembly. Ou seja, da muito mais trabalho. Um exemplo clássico é o código das implmentações do memcpy em produção, mega complexos e cheios de assembly.

Dessa forma é um grande error optar por C++ esperando performance e não incluir esse tipo de trabalho no escopo do projeto.

2

Legal, gostei da sua postagem, até porque provavelmente eu nunca veria isso sem ela.

Primeiro, uma coisa que todo mundo precisa saber que nem todo estudo prova alguma coisa. Na verdade poucos possuem provas, a maioria especulam com alguma fundamentação (alguns nem isso), alguns são bem mal conduzidos, nunca validados e até mesmo é comum serem contestados por outro estudo. Fotra quando apenas o foco é diferente ou o critério não é o mesmo.

Vou dar um exemplo dos estudos que estabelecem o limite de pessoas que o planeta suporta. Os que mais gosto são os que falam em 4 bilhões de pessoas dentro da economia atual (se bem que isso não muda há anos, e não acho que tenha tanta credibilidade assim) e outros que diz que é 1.5 bilhão usando o critério de todas as pessoas terem consumo uniforme nivelado pelos países desenvolvidos. Tem estudo que fala em trilhões de pessoas, o critério deve ser quantas cabem em pé na superfície terrestre.

Estudo sério tem uma forma bastante rígida de ser realizado e publicado. E só passa ter um valor maior quando é publicado por algum veículo científico. Hoje em dia muiutos desses veículos são apenas meios comerciais para dar valor para estudos extremamente fracos, criou-se um mercado para atender os pesquisadores que precisam de uma validação que antes já era corporativista e agora é meramente quuem pode pagar. Tem publicações muito sérias também.

Em gerl nós não acessamos estudos mais sérios, eles chatos, difíceis de entender se não por um pesquisador na área (inclusive muitas validações para publicação são feitas por quem não entende do que está sendo tratado ali).

Claro que podemos ter bons estudos sem toda essa pompa também. Não estou invalidando o estudo que o cara fez, até porque eu precisaria de bastante tempo para isso. Só estou colocando as coisas no lugar para os deslumbrados não teimosos que não tem mais solução entenderem o contexto, até porque tudo depende de contexto.

Dito isso, um JIT sempre tem potencial de conseguir executar algo melhor que um AOT porque ele pode avaliar a execução e fazer otimizações que só fazem sentido para aquele padrão que está ocorrendo isso. Isso não quer dizer que é fácil fazer isso, que compense sempre, e que em alguns casos a análise e regeração do código otmizado pode matar o ganho obtido ou até piorar o desempenho.

Os casos em que o ganho costuma ser frequente porque o padrão se repete é possível ter as mesmas otimizações com AOT se usar PGO (Profile Guided Optimization). E é possível até mais de uma versão de código para padrões diferentes que podem ocorrer, ainda que isso vai engordar seu executável e ter um custo para escolher qual usar. Isso não costuma ser usado porque normalmente não se quer esse preço quando se usa AOT.

Se você conseguisse comparar um AOT e um JIT em condições absolutamente iguais, na esmagadora maioria dos casos o AOT te daria um resultado melhor porque seria comum que as execuções seriam sempre dentro do mesmo padrão. Quando você compara um compilador JIT com um AOT completamente diferente, é o mesmo que comparar um JIT com outros JIT completamente diferente, a chance maior é que a diferença de implementação é que esteja fazendo diferença e não o modelo de compilação.

Por isso estudos precisam ser lidos com cuidado, assim o ovo pode fazer bem ou mal para sua saúde e ambos estão certos. A vida não é o manequismo que cada vez mais as pessoas desejam que exista. Não existem respostas tão absolutas quanto as pessoas querem, por isso que alguns brincam que a senioridade é medida de acordo com a quantidade de "dependes" que ele fala. Se quando você faz o estudo se errar uma vírgula na definição de critério o estudo já pode estar inteiro furado.

Olhando por cima esse estudo mostra uma compração de uma situação encontrada. Não vou questionar se está certo ou errado porque eu teria muito trabalho para fazer isso de forma adequada, correria o risco de errar também, mas quero ressaltar que ele mostra só o que está aí, qualquer especulação que o JITando já consegue ser melhor que Ahead Of Time não pode ser feita.

Por isso que eu semrpe falo que linguagem não possui velocidade. No máximo a implmentação possui. Claro que algumas características colocadas na sua específicação podem facilitar ou dificultar ter perfroamnce, mas é possível ter mecanismos que melhorem o resultado ou que piore muito sem necessidade. E você pode fazer um teste que uma implementação de uma linguagem ganha de outra implmentação da mesma linguagem ou de outra linguagem e uma mudança mínima no código pode dar o resultado contrário. Mais ainda, em uma nova versão da implementação o resultado pode ser outro.

Eu acho ótimo que o JITters estejam melhorando, até porque ele sempre vão puxar o AOT+PGO para cima junto e todos saem ganhando.

Um dos grandes problemas dos compuladores Just In Time é que ele engordam o executável e gastam mais energia para executar, nem sempre você quer isso. Mas ele pode trazer benefícios que enxugam o código total e economizar energia ou outros recursos.

Em tempos de uso mobile, IoT, serverless e outras coisas pagas pelo uso, e até a IA, escolher o melhor modo de compilação, e a tecnologia mais adequada, sempre que possível, é outra skill que o bom programador deve ter.

Em se tratando de linguagens diferentes pode ser que para obter o melhor resultado precise fazer um código diferenet na outra linguagem. Quase sempre o código já é um pouco difeente, então a comparação não é exatamente banans com bananas, nem mesmo banana prata com banana prata. O Clang pode ser assim, mas o GCC ou VSC pode ser de outro jeito, ou ainda a próxima versão do Clang pode dar um resultado diferente, até mesmo pode ser uma versão mais antiga que dava e hoje não dá mais, embora seja menos provável.

Equivalência de código pode ser ótmo para comparar algo, mas pode ser justamente o que destrói o resultado.

Me lembro de um caso de muitos anos atrás que um funcionário da Microsoft fez uma versão de um software de um dos maiores figurões da empresa, bem conhecido em que ele portou para C# o que era original em C++. Na época o JITter do .NET era ruim. E o código em C# ficou mais rápido. O "japonês" figurão da Microsoft começou mexer no dele em C++, introduziu bugs no processo e teve um trabalhaão para chegar em uma versão (a sexta) que batesse o C# de forma definitiva. Vou contar essa história em detalhes no meu canal que estreará em breve.

É quase certo que sempre você conseguirá obter um resultado melhor em C++ do que em Java ou C#, aina mais no estado que eles estão hoje que ainda tem muito potencial para melhor (até C++ ainda tem) nas otimizações. Mas o trabalho será cada vez maior para conseguir.

Você já viu como a STL é implementadas nas bibliotecas dos diversos compiladores? Você não consegue entender nada do código, é algo de uma complexidade absurda. Por que alguém faria isso? Para obter a melhor performance. Vale o esforço porque o mundo todo se beneficiará dele.

Para te ajudar bastante teria que fazer uma análise microscópica, analisar o código final gerado que realmente executa para descobrir o que aconteceu. Mas já posso dizer que a compração ficaria mais justa se fizer um PGO (coisa que nunca fiz em C++).


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).

2

É quase certo que sempre você conseguirá obter um resultado melhor em C++ do que em Java ou C#, aina mais no estado que eles estão hoje que ainda tem muito potencial para melhor (até C++ ainda tem) nas otimizações. Mas o trabalho será cada vez maior para conseguir.

O trabalho demorou menos de um dia. O código está todo disponível no GitHub. Qualquer um pode baixar e rodar na sua máquina. Fazer os seus testes. Tirar as suas próprias conclusões. Refutar ou aceitar os resultados obtidos, com argumentos e provas técnicas.

Ou seja, seria legal matar a cobra e mostrar o pau, caso contrário estamos apenas tendo uma conversa de bar :) Ou seja, afirmar que C++ é mais rápido e pronto, e o que aconteceu aí foi uma quebra das leis da física ou um evento paranormal não vai ajudar na pesquisa nem no aprendizado :) Pode ter sido um erro/imprecisão do benchmarking? Pode. E estamos atrás desse erro ou dessa imprecisão. Assim como estamos atrás dos alienígenas. Brincadeiras a parte, nós fizemos os testes e colocamos eles disponíveis. Se uma IntMap em C++ pode ser mais rápida que uma IntMap em Java, então gostaríamos de ver isso na prática e não na teoria.

Há poucos dias atrás houve um tópico parecido que viralizou. O nosso amigo Pedro Pessoa, que é um cara muito inteligente, fez um video excelente sobre isso aqui => https://www.youtube.com/watch?v=YI0k6bGRuzk

Gostaria sinceramente de entender o porquê desse código em C++ ser mais lento que o mesmo código em Java. Seria JIT melhor que AOT em muitos casos como esse? Ou dá para fazer alguma coisa no compilador de C++ para otimizar esse código?

Tem um discussão rolando em outro forum onde levantaram a questão de como o C++ está alocando a memória no heap. E que ele não faz isso de uma maneira tão eficiente quanto a JVM. Eu realmente ficaria surpreso caso não houvesse uma maneira de fazer esse código simples de C++ performar no mesmo nível que o código Java. Até porque no final, tudo vai para assembly.

1

Indo direto ao ponto: o motivo do jit ser mais rápido é por que ele esta alocando memoria de forma muito mais eficiente, do que fazer um malloc/free por elemento. Que são operacões extramamente lentas!!!

Se estiver curioso em entender o que esta acontecendo, execute ambos os executaveis com o dtrace por exemplo ou com outra instrumentação para ver as syscalls sendo feitas. Quase sempre isso é o fator determinante!

2

Boa dica! Uma maneira de provar/desprovar isso seria pre-allocar um monte de Entry objects e rodar o benchmark dos puts com os objetos já pre-allocados pelo C++. Concorda?

Agora isso explicaria a vantagem do put, mas não explica a vantagem do get e do remove. Concorda?

E o mais importante: não tem jeito de resolver essa ineficiência de C++ na alocação de memória no heap? A HotSpot JVM vai sempre levar vantagem nisso? E aquela história que o C++ te dá controle total de tudo, etc e tals?

Estou perguntando na boa. Quero entender as vantagens e desvantagens de cada linguagem (C++ e Java) e cada approach (AOT e JIT).

1

Boa dica! Uma maneira de provar/desprovar isso seria pre-allocar um monte de Entry objects e rodar o benchmark dos puts com os objetos já pre-allocados pelo C++. Concorda?

Teste e compartilhe os resultados. Acredito que pre-alocar todos os objetos em memória estática resultaria em uma melhora de desempenho de várias ordens de grandeza.

Agora isso explicaria a vantagem do put, mas não explica a vantagem do get e do remove. Concorda?

A explicação para a vantagem do JIT em todas as operações reside na redução da fragmentação de memória, que leva ao uso eficiente do cache.

E aquela história que o C++ te dá controle total de tudo, etc e tals?

Exatamente. A otimização da alocação de memória é uma responsabilidade do programador. Isso envolve técnicas como alocação em blocos, uso de pools de memória e até mesmo a implementação de alocadores personalizados. O compilador, neste caso, não vai oferecer otimizações significativas.

1

Exatamente. A otimização da alocação de memória é uma responsabilidade do programador. Isso envolve técnicas como alocação em blocos, uso de pools de memória e até mesmo a implementação de alocadores personalizados. O compilador, neste caso, não vai oferecer otimizações significativas.

Entendi. Então possível é, só exige uma eforço e uma complexidade muito maior para conseguir algo que a HotSpot JVM/JIT te dá de mão beijada.

Prealocar tudo no pool (note no código que os Entries ficam numa linked-list que é um pool) antes de começar a executar é inconveniente. Até porque muitas vezes vc não vai saber a quantidade média/máxima com antecedência.

Agora fiquei curioso/interessado em MELHORAR esse código. Em consertá-lo para ele ser tão performático quanto a versão em Java. Isso é possível ou é melhor escrever isso em assembly? :P Vc mencionou alocação em blocos e até mesmo a implementação de alocadores personalizados. Isso é possível de se fazer para tornar esse código mais rápido?

Engraçado que as pessoas abrem a boca para falar que C++ é a coisa mais performática do mundo, blah, blah, blah. Se vc é DEUS talvez ele na sua mão seja bem performático mesmo. Se para implementar uma simples hash table você precisa pensar em 10 complexidades extras que com o Java vc não precisa nem saber que existem, realmente eu me pergunto como grandes empresas mantem projetos de C++ grandes, que envolvem vários programadores, com qualidade. Talvez o KERNEL do Linux tenha dado certo, poque por muito tempo o único que meteu a mão ali foi o Linus.

Abstração é muito importante. E Java te dá isso sobre C++ de uma maneira muito eficiente e performática. O JIT resolve muita coisa. Se vc quiser baixar o nível para "controlar melhor a performance" então vc deve ser um DEUS e nesse caso melhor partir direto para o assembly. A impressão que fica é que se você for apenas um bom/ótimo programador, o resultado final não vai ficar bom se comparado como poderia ter ficado com uma linguagem mais de alto nível (e igualmente performática) como o Java. Não vai ficar bom tanto do ponto de vista da performance como do ponto de vista da organização (clareza do código, ausência de bugs, ausência de vulnerabilidades, etc). Não vamos nem entrar na questão de que C++ é o paraíso das vulnerabilidades e até o governo americano está querendo se livrar dele nos seus sistemas.

Repare que no GitHub a gente também testou GraalVM compilando o Java para nativo ahead-of-time. Mas não fica mais rápido que a HotSpot JIT.

2

Vc mencionou alocação em blocos e até mesmo a implementação de alocadores personalizados. Isso é possível de se fazer para tornar esse código mais rápido?

Uma abordagem muito simples e ingênua seria, em vez de alocar cada entrada individualmente, alocar, digamos, 1000 de uma vez. Ou simplesmente alocar, digamos, 1 KB de memória, seria mais inteligente e alinhar o número de elementos para caber nesse tamanho de bloco predefinido. E se você estiver realmente preocupado com desempenho, poderia ajustar esse tamanho para preencher a cache L1 do seu processador.

Se você quiser ficar mais sofisticado, consulte a documentação do cppreference para o alocador polimórfico e etc. Ou se você quiser ir ainda mais fundo, estude a documentação do Linux para brk e implemente seu próprio malloc com ele. Ou não.

Se vc quiser baixar o nível para "controlar melhor a performance" então vc deve ser um DEUS e nesse caso melhor partir direto para o assembly.

Novamente, entender quais chamadas do sistema acontecem e quando é a primeira coisa a começar a entender e pensar sobre desempenho. Não, não precisa ser um deus. Mas tem que saber de sistemas operacionais e organização de computadores, no mínimo.

Programar em assembly, é uma grande perda de tempo. Compiladores modernos são altamente otimizados e geralmente superam em muito capacidade de micro-otimização humana.

No entanto, quando desempenho é crítico, uma prática essencial é examinar o código assembly gerado pelo compilador para identificar possíveis gargalos e oportunidades de otimização. Algumas vezes, o compilador pode falhar em gerar o código mais eficiente. Em muitos casos, alterar a construção da linguagem de alto nível é suficiente.

Além disso, temos as instruções mágicas (como as vetorias) de processadores atuais. Compiladores são bastante inteligentes e frequentemente otimizam o código para usar essas instruções sem intervenção explícita do programador, mas, em muitos casos, é necessário utilizá-las diretamente em código C/C++ de forma inline.

Um abraço e bons estudos!

1

E se você estiver realmente preocupado com desempenho, poderia ajustar esse tamanho para preencher a cache L1 do seu processador.

Com o Java também dá para fazer isso. Tenho um exemplo usando o Java heap e outro usando memória nativa via sun.misc.Unsafe.

Não é super complexo quanto parece. Tudo que vc tem que fazer é espaçar as coisas para isolá-las dentro de um cache line do CPU, de forma que outras coisas não te expulsem do cache sem que vc precisasse ter saído do cache. Para isso vc pode usar padding.

Acho que o disruptor foi o primeiro que fez isso com Java, mas posso estar errado.