
Hoje terminou a décima segunda JVM LS Summit. Como sempre, foi um evento hardcore com apresentações técnicas em máquinas virtuais e os idiomas que são executados nelas. Como de costume, a cúpula foi realizada em Santa Clara, no campus da Oracle. Como sempre, há muito mais pessoas que desejam chegar aqui do que lugares: o número de participantes não excede 120. Como sempre, não houve marketing, apenas miudezas.
Esta cúpula já é a terceira para mim, e toda vez que a visito com grande prazer, apesar do terrível jetlag. Aqui você pode ouvir não apenas relatórios, mas também conhecer pessoas melhores do mundo da JVM, participar de conversas informais, fazer perguntas em workshops e geralmente se sente envolvido em grandes realizações.
Se você não compareceu à cúpula, isso não importa. A maioria dos relatórios é publicada no YouTube quase imediatamente após a cúpula. Na verdade, eles já estão disponíveis . Para facilitar a navegação, descreverei brevemente aqui todos os relatórios e workshops que consegui participar.
29 de julho
Não se trata dos recursos da compilação do futuro na linguagem Clojure , como muitos pensavam, mas simplesmente do desenvolvimento da linguagem, dos meandros da geração de código e dos problemas que eles encontram. Por exemplo, no Clojure, é importante anular variáveis locais após o último uso, porque, se o cabeçalho de uma lista que é gerado lentamente em uma variável local, ignorando-o, os nós que já foram ignorados podem não ser coletados pelo coletor de lixo e o programa pode falhar com o OutOfMemory . Em geral, o próprio compilador C2 JIT libera as variáveis após o último uso, mas o padrão não garante isso e, por exemplo, o interpretador HotSpot não.
Também foi interessante aprender sobre a implementação do despacho dinâmico de chamadas de função. Também aprendi que, até recentemente, Clojure estava direcionado à JVM 6 e somente recentemente mudou para a JVM 8. Agora, os autores do compilador analisam o invokedynamic.
O projeto Loom é uma fibra leve para Java. Um ano atrás, Alan e Ron já conversaram sobre esse projeto, e então parecia que tudo estava indo muito bem e estava prestes a ficar pronto. No entanto, esse projeto ainda não entrou oficialmente no Java e ainda está sendo desenvolvido em uma bifurcação separada do repositório. Obviamente, foi necessário resolver muitos detalhes.
Muitas APIs padrão de ReentrantLock.lock a Socket.accept já estão adaptadas para fibras: se essa chamada for feita dentro de uma fibra, o estado de execução será salvo, a pilha será desenrolada e o encadeamento do sistema operacional será liberado para outras tarefas até que um evento desperte a fibra (por exemplo, ReentrantLock.unlock). No entanto, por exemplo, o bom e velho bloco sincronizado ainda não funciona e, ao que parece, não há como refatorar seriamente todo o suporte à sincronização na JVM. Outro desenrolamento da pilha não funcionará se houver quadros nativos na pilha entre o início da fibra e o ponto de interrupção. Nos dois casos, nada explode, mas a fibra não libera o fluxo.
Há muitas perguntas sobre como o Fiber se compara à antiga classe java.lang.Thread. Há um ano, havia uma idéia de fazer do Fiber uma subclasse de Thread. Agora eles a recusaram e a tornaram uma entidade independente, porque emular em todas as fibras todo o comportamento de um fluxo regular é bastante caro. Nesse caso, Thread.currentThread () dentro da fibra retornará o blende gerado, e não o encadeamento real no qual tudo é executado. Mas o obstáculo se comportará muito bem (embora possa retardar o trabalho). A idéia importante é nunca fornecer o fluxo de mídia real no qual a fibra está sendo executada dentro da fibra. Isso pode ser perigoso, pois uma fibra pode ser movida facilmente para outro segmento. A decepção continuará.
É curioso que os participantes do projeto já tenham introduzido algumas alterações preparatórias no repositório principal do JDK para facilitar sua vida. Por exemplo, no Java 13, o método doPrivileged foi reescrito inteiramente a partir do código nativo em Java, obtendo um aumento de cerca de 50 vezes no desempenho. Por que este é um projeto Loom? O fato é que esse método específico muitas vezes aparece no meio da pilha e, embora fosse nativo, as fibras dessa pilha não pararam. De uma forma ou de outra, o projeto já está se beneficiando.
Na página do projeto, você pode ler a documentação e fazer o download da árvore de origem, e também existem assemblies binários que você pode executar e reproduzir hoje. Esperamos que nos próximos anos tudo seja integrado.
Brian Goetz - Workshop "Projeto Âmbar"
Paralelamente, estava sendo realizado um workshop sobre o projeto Loom, mas eu fui para Amber. Aqui discutimos brevemente os objetivos do projeto e os principais PEC em que o trabalho está em andamento - correspondência de padrões , registros e tipos selados . Então toda a discussão caiu na questão privada do escopo. Eu falei sobre isso na conferência do Coringa no ano passado, em princípio, nada de novo foi dito. Tentei fazer uma ideia com tipos de união implícitos, como if(obj instanceof Integer x || obj instanceof Long x) use(x.longValue())
, mas não vi entusiasmo.
Em todos os aspectos, um projeto maravilhoso do Google para procurar corridas usando dados na forma de leitura e gravação do mesmo campo não volátil ou elemento de matriz de diferentes fluxos sem estabelecer um relacionamento de antes do acontecido. O projeto foi originalmente escrito como um módulo LLVM para código nativo e agora foi adaptado para o HotSpot. Este é um projeto oficial do OpenJDK com sua lista de discussão e repositório.
Segundo os autores, a coisa agora está funcionando, você pode montar e brincar. Além disso, ela encontra corridas não apenas no código Java, mas também no código das bibliotecas nativas. As corridas no código da própria máquina virtual não são pesquisadas, porque todas as primitivas de sincronização são gravadas à sua maneira e o TSan não pode detectá-las. Segundo os autores, o TSan não fornece falsos positivos.
O principal problema é o desempenho. Agora, apenas o intérprete é instrumentado para o código Java, respectivamente, a compilação JIT está completamente desativada e o intérprete, que já é lento, diminui a velocidade várias vezes. Mas se você tiver recursos suficientes (o Google, é claro, o suficiente), ocasionalmente poderá conduzir seus conjuntos de testes usando o TSan. Também está planejado adicionar instrumentação à JIT, mas essa é uma intervenção muito mais séria na JVM.
Alguém perguntou se a desativação da compilação JIT não afeta o resultado, porque algumas raças podem não aparecer no intérprete. O orador não descartou essa possibilidade, mas disse que eles já haviam encontrado um grande número de corridas que levariam muito tempo para serem feitas. Portanto, tenha cuidado ao executar seu projeto no TSan: você pode descobrir a verdade desagradável.
Todo mundo está esperando por tipos de valor em Java, mas ninguém sabe quando eles aparecerão. No entanto, os movimentos são cada vez mais graves. Já existem montagens binárias de teste com o marco L2 atual. Nos planos atuais, o Valhalla completo chegará ao marco da L100, mas os autores ainda estão otimistas e acreditam que mais de dois por cento foram realizados.
Portanto, do ponto de vista da linguagem, temos classes com o modificador inline, que são processados de maneira especial pela máquina virtual. Instâncias de tais classes podem ser incorporadas em outros objetos, e matrizes simples contendo instâncias de classes embutidas também são possíveis. A instância não possui um cabeçalho, o que significa que não há identidade, o código de hash é calculado por campos, ==
também por campos, uma tentativa de sincronização ou Object.wait()
nessa classe gerará uma IllegalMonitorStateException. Gravar null
em uma variável desse tipo, é claro, não funcionará. No entanto, os autores oferecem uma alternativa: se você declarou um Point
classe em linha, pode declarar um campo ou uma variável do tipo (surpresa-surpresa!) Point?
e, em seguida, haverá um objeto completo no heap (como boxe) com um cabeçalho, identidade e null
.
Perguntas abertas sérias continuam sendo a especialização de genéricos e a migração de classes existentes (por exemplo, Optional
) para uma classe embutida para não quebrar o código existente (sim, as pessoas escrevem null
em variáveis do tipo Optional
). No entanto, a imagem aparece e a diferença é visível.
Foi uma surpresa para mim que o mesmo Neil Gufter, co-autor dos quebra-cabeças Java originais, agora trabalhe na Microsoft on .Net runtime. Também foi uma surpresa ver um relatório sobre o CLR (o chamado .Net runtime) na JVM LS. Mas familiarizar-se com a experiência de colegas de outros mundos é sempre útil. O relatório fala sobre as variedades de referências e ponteiros no CLR, sobre as instruções de bytecode usadas para tipos de valor e sobre como funções generalizadas lindamente especializadas como reduzir. Foi interessante saber que um dos objetivos dos tipos de valor em .Net é uma interoperabilidade com código nativo. Por esse motivo, a localização dos campos nos tipos de valor é estritamente fixa e pode ser projetada em uma estrutura sem transformações. A JVM nunca teve essa tarefa e o que fazer com a interoperabilidade nativa - veja abaixo.
Atualize novamente o relatório do ano passado . Novamente, a pergunta é por que eles ainda não lançaram nada, se há um ano tudo parecia muito bom.
Um vetor é uma coleção de vários números, que no hardware podem ser representados por um único registro de vetor como zmm0 para AVX512. Nos vetores, você pode carregar dados de matrizes, executar operações neles como multiplicação por elementos e jogá-los de volta. Todas as operações para as quais existem instruções do processador são intrinsizadas pelo compilador JIT nessas instruções. O número de operações é simplesmente enorme. Se algo estiver faltando, uma implementação lenta alternativa é usada. Idealmente, objetos intermediários de vetor não são criados; a análise de escape funciona. Todos os algoritmos de computação padrão são vetorizados com um estrondo, usando toda a potência do seu processador.
Infelizmente, é difícil para os autores não terem valgalla: a análise de escape é frágil e pode não funcionar facilmente. Esses vetores simplesmente devem ser classes embutidas, para que todos os problemas desapareçam. Não está claro se essa API pode ser lançada antes da primeira versão do Valgalla. Parece muito mais pronto. Entre os problemas chamados dificuldades com o suporte do código. Existem muitas partes repetidas para diferentes tamanhos de registradores e diferentes tipos de dados; portanto, a maior parte do código é gerada a partir de modelos e é difícil mantê-lo.
O uso também é imperfeito. Não há sobrecarga de operador em Java; portanto, a matemática parece feia: em vez de max(va-vb*42, 0)
você deve escrever va.lanewise(SUB, vb.lanewise(MUL, 42)).lanewise(MAX, 0)
. Seria bom ter acesso às lambdas AST como em C #. Em seguida, seria possível gerar uma operação lambda personalizada como MYOP = binOp((va, vb) -> max(va-vb*42, 0))
e usá-la.
30 de julho
O segundo dia passou sob a bandeira da compilação.
Um funcionário da IBM, membro do projeto JVM OpenJ9, fala sobre sua experiência com a compilação JIT e AOT. Sempre há problemas: o JIT é uma inicialização lenta, porque está esquentando; Custos de CPU para compilação. AOT - desempenho abaixo do ideal devido à falta de um perfil (é possível criar um perfil, mas de maneira não trivial e nem sempre o perfil durante a compilação corresponde ao perfil na execução), é mais difícil de usar, vincula-se à plataforma de destino, SO, coletor de lixo. Alguns dos problemas podem ser resolvidos combinando abordagens: começando com o código compilado pela AOT e finalizando com o JIT. Uma boa alternativa para tudo isso é o cache do JIT. Se você tem muitas máquinas virtuais (olá, microsserviços), todas elas recorrem a um serviço separado - o compilador JIT (sim, JITaaS), onde tudo é como um adulto, orquestração e balanceamento de carga. Este serviço é compilado. Muitas vezes, ele pode fornecer código pronto para um determinado método, porque esse método já foi compilado em outra JVM. Isso melhora muito o aquecimento, remove o consumo de recursos do seu serviço JVM e geralmente reduz o consumo total de recursos.
Em geral, o JITaaS poderia ser a próxima palavra da moda no mundo da JVM. Infelizmente, não entendi se isso poderia ser reproduzido no momento ou se ainda é um desenvolvimento fechado.
O GraalVM Native Image é um aplicativo Java compilado em código nativo que é executado sem a JVM (diferente dos módulos compilados usando um compilador AOT como o jaotc). Mais precisamente, esse não é um aplicativo Java. Para funcionar corretamente, ele precisa de um mundo fechado, ou seja, todo o código deve estar visível no estágio de compilação, sem Class.forName. Você pode refletir e manipular métodos, mas quando compila, precisa informar especificamente quais classes e métodos serão usados por meio da reflexão.
Outra coisa divertida é a inicialização da classe. Muitas classes são inicializadas durante a compilação. Ou seja, digamos, seus campos estáticos serão computados por padrão pelo compilador e o resultado será gravado na imagem montada e, quando você inicia o aplicativo, ele é simplesmente lido. Isso é necessário para obter melhor qualidade de compilação: qualquer dobra constante pode ser feita se os valores dos campos estáticos forem conhecidos pelo compilador. Tudo está bem com o JIT, o intérprete executa a inicialização estática e, depois de conhecer as constantes, você pode compilar. E ao criar um aplicativo nativo, você precisa enganar. Isso, é claro, leva a divertidos efeitos psicodélicos. Portanto, as classes geralmente são inicializadas na ordem em que são acessadas e, durante a compilação, essa ordem é desconhecida e a inicialização em outra é possível. Se houver referências circulares entre inicializadores de classe, você poderá ver a diferença no comportamento do código da JVM e na imagem nativa.
Workshop Schatzl - Hotspot GC.
Resolvemos toda a dor associada aos coletores de lixo. Infelizmente, eu ouvi a maioria. Lembro-me de que a recuperação da memória do sistema operacional foi discutida, incluindo o Xmx nojento para todos. Há boas notícias: no Java 13, uma nova opção -XX é adicionada: SoftMaxHeapSize. Até o momento, ele é suportado apenas pelo coletor ZGC, mas o G1 também pode recuperar o atraso. Ele define um limite para o tamanho da pilha, que não deve ser excedido, exceto em situações de emergência, quando não funciona de maneira diferente. Assim, você pode definir um Xmx grande (digamos, igual ao tamanho de toda a RAM) e um SoftMaxHeapSize razoável. Em seguida, a JVM se manterá na maior parte do tempo, mas, no pico de carga, ainda não lançará OutOfMemoryError, mas consumirá mais memória do sistema operacional. Quando a carga cair, a memória retornará.
O Microsoft Mei-Chin Tsai falou sobre os recursos da compilação JIT e AOT no CLR. A compilação AOT vem sendo desenvolvida há muito tempo, mas inicialmente (ngen.exe) foi realizada na plataforma de destino, como na primeira vez em que foi iniciada (se você tiver o Windows, procure os arquivos * .ni.dll na pasta Windows). Os arquivos são obtidos dependendo da versão do Windows local e até de outras DLL-ek. Portanto, se a dependência for atualizada, todos os módulos nativos deverão ser recompilados. Na segunda geração (crossgen), os autores pré-compilaram aplicativos e módulos relativamente independentes das versões e dependências de hardware e SO. Isso diminuiu a velocidade do código porque as chamadas de dependência agora tinham que ser feitas honestamente virtuais. Esse problema foi resolvido conectando o JIT e recompilando o código quente durante o aplicativo. Depois, conversamos sobre a compilação em vários níveis (em camadas) (parece que no CLR isso ainda está engatinhando, enquanto ele se desenvolve em Java há pelo menos dez anos) e sobre planos futuros para tornar a AOT verdadeiramente multiplataforma.
Os colegas do Alibaba apresentaram sua abordagem ao problema de aquecimento da JVM. Eles usam a JVM para muitos serviços da web. Em princípio, uma inicialização muito rápida não é tão importante, porque o balanceador sempre pode esperar até a máquina inicializar e só então começar a enviar solicitações para ela. No entanto, o problema é que a máquina não aquece sem solicitações: o código que descreve a lógica para processar solicitações não é chamado, o que significa que não é compilado. Ele será compilado quando as primeiras solicitações chegarem, ou seja, não importa quanto o balanceador espere, haverá uma falha de desempenho nas primeiras solicitações. Anteriormente, eles tentavam resolver isso lançando solicitações falsas para o próximo serviço antes de enviar solicitações reais para ele. A abordagem é interessante, mas é bastante difícil gerar um fluxo tão falso que causaria a compilação de todo o código necessário.
Um problema separado é a desoptimização. Nas primeiras mil consultas, uma if
sempre percorresse a primeira ramificação, o compilador JIT geralmente fazia a segunda, inserindo uma armadilha de desotimização para reduzir o tamanho do código. Mas a 1001ª solicitação foi para a segunda ramificação, a desoptimização funcionou e todo o método foi para o intérprete. Enquanto as estatísticas estão sendo compiladas novamente, enquanto o método é compilado pelo compilador C1, e pelo perfil completo pelo compilador C2, os usuários experimentam uma desaceleração. E então, no mesmo método, outro if
pode ser desoptimizado, e tudo dará um novo.
O JWarmUp resolve o problema da seguinte maneira. Durante a primeira execução do serviço, um registro de compilação é gravado por vários minutos: registra quais métodos foram compilados e as informações de perfil necessárias por ramificações, tipos etc. Se esse serviço for reiniciado, imediatamente após a inicialização, todas as classes do log serão inicializadas e os métodos registrados serão compilados. levando em consideração o perfil anterior. Como resultado, o compilador funcionará bem na inicialização, após o qual o balanceador começará a enviar solicitações para esta JVM. Nesse momento, todo o código quente que ela já foi compilado.
Vale a pena notar que o problema de início rápido não é resolvido aqui. Um lançamento pode ser ainda mais lento porque muitos métodos são compilados, alguns dos quais podem ser necessários apenas alguns minutos após o lançamento. Mas o log acaba sendo reutilizável: diferente da AOT, você pode aumentar o serviço em uma arquitetura diferente ou com um coletor de lixo diferente e reutilizar o log anterior.
Os autores tentam há muito tempo inserir o JWarmUp no OpenJDK. Até agora sem sucesso, mas o trabalho está se movendo. O principal é que um patch completo é bastante acessível para você no servidor de Revisão de Código, para que você possa aplicá-lo facilmente às fontes do HotSpot e criar a JVM com o JWarmUp.
Este é um trabalho de pesquisa de Manchester, mas os autores afirmam que o projeto já foi implementado em alguns lugares. Também é um complemento para o OpenJDK, o que facilita bastante a transferência de determinados códigos Java para GPU, iGPU, FPGA ou simplesmente o paralela aos núcleos de seu processador. Para compilar na GPU, eles usam o GraalVM no qual construíram seu back-end - TornadoJIT. Um método Java corretamente escrito, transparente, vai para o dispositivo correspondente. É verdade que eles dizem que a compilação no FPGA pode levar várias horas, mas se sua tarefa é considerada um mês, por que não? Alguns benchmarks (por exemplo, a transformação discreta de Fourier) são cem vezes mais rápidos que o Java simples, o que é esperado em princípio. O projeto é totalmente carregado no GitHub , onde você também pode encontrar publicações científicas sobre o tópico.
A mesma música - um projeto de longa data, todas as apresentações da cúpula, um ano atrás, tudo parecia bem pronto, mas ainda não havia lançamento. Acontece que, desde então, o foco mudou.
A idéia do projeto é um interope melhorado com código nativo. Todo mundo sabe como é doloroso usar o JNI. Isso realmente dói. O projeto Panamá anula esse problema: o uso de classes Java jextract são geradas a partir dos arquivos * .h da biblioteca nativa, que são bastante convenientes de usar chamando métodos nativos. No lado C / C ++, você não precisa escrever uma única linha. Além disso, tudo ficou muito mais rápido: a sobrecarga nas chamadas para Java-> nativo e nativo-> Java caiu às vezes. O que mais você poderia querer?
Há um problema que existe há algum tempo - a transferência de matrizes de dados para o código nativo. Até agora, o método recomendado é o DirectByteBuffer, que apresenta muitos problemas. Uma das mais graves é a vida útil não gerenciada (o buffer desaparece quando o coletor de lixo pega o objeto Java apropriado). Devido a esse e outros problemas, as pessoas usam o Inseguro, que, com a devida diligência, pode facilmente estabelecer toda a máquina virtual.
Isso significa que você precisa de um novo acesso normal à memória fora do heap Java. Alocação, acessadores estruturados, remoção explícita. Acessadores estruturados - para que você não precise calcular as compensações por conta própria se precisar escrever, por exemplo, struct { byte x; int y; }[5]
struct { byte x; int y; }[5]
struct { byte x; int y; }[5]
Em vez disso, você descreve uma vez o layout dessa estrutura e, por exemplo, VarHandle
, que pode ler todos os x
saltando sobre y
. Nesse caso, é claro, sempre deve haver verificação de borda, como em matrizes Java comuns. Além disso, deve haver uma proibição de acesso a uma área já fechada. E isso acaba sendo uma tarefa não trivial, se queremos manter o desempenho no nível Inseguro e permitir o acesso a partir de vários encadeamentos. Em suma, assista ao vídeo, muito interessante.
Workshop: Vladimir Kozlov - Projeto Metropolis
O projeto Metropolis combina todas as tentativas de reescrever partes da JVM em Java. Hoje, sua parte principal é o compilador Graal. Nos últimos anos, ele se desenvolveu muito bem e já se fala de um substituto completo para o envelhecimento do C2. Costumava haver um problema de bootstrap: o graal começava devagar, porque ele próprio tinha que ser compilado ou interpretado pelo JIT. Em seguida, a compilação do AOT apareceu (sim, o principal objetivo do projeto de compilação do AOT é o bootstrap do próprio graal). Mas com o AOT, o grail consome uma parte decente do heap de um aplicativo Java que pode não querer compartilhar seu heap. Agora aprendemos a transformar o graal em uma biblioteca nativa usando o Graal Native Image, o que nos permitiu isolar o compilador da pilha geral. Com o desempenho máximo do código compilado pelo graal, ainda existem problemas em alguns benchmarks. Por exemplo, o graal fica atrás de C2 em intrínseca e vetorização. No entanto, graças à poderosa análise de inlining e escape, ela simplesmente quebra C2 no código funcional, onde muitos objetos imutáveis e muitas funções pequenas são criadas. Se você escreve sobre a rocha e ainda não usa o graal, corra para usá-lo. Além disso, nas versões mais recentes do JDK, é bastante trivial fazer algumas chaves, tudo já está no kit.
31 de julho
Kevin Bourrillion - Anotações de nulidade para Java
Kevin anunciou um novo projeto, mas pediu para não falar publicamente e não publicar uma gravação de seu discurso no YouTube. Sinto muito. , .
Sorbet (!) Ruby, Ruby . , Stripe Ruby , , . , .
Lightning Talks
- . Remi Forax , , . , :
, - , .
ML AI , . , Facebook — getafix , --, , . . , , . , , .
. . OpenJDK Committer Workshop.