paint-brush
O software prospera a menos que você o elimine primeiro: otimização prematura e uma história de Java GCpor@wasteofserver
553 leituras
553 leituras

O software prospera a menos que você o elimine primeiro: otimização prematura e uma história de Java GC

por Frankie6m2024/04/06
Read on Terminal Reader

Muito longo; Para ler

Não exagere nas otimizações, deixe a linguagem trabalhar para você. Contar história. As atualizações Java aumentam o desempenho gratuitamente. Referência, sempre.
featured image - O software prospera a menos que você o elimine primeiro: otimização prematura e uma história de Java GC
Frankie HackerNoon profile picture
0-item

Um LinkedList será mais rápido? Devo trocar o `for each` por um `iterador`? Este `ArrayList` deveria ser um `Array`? Este artigo surgiu em resposta a uma otimização tão malévola que ficou permanentemente gravada em minha memória.


Antes de entrarmos de frente em Java e nas maneiras de lidar com a interferência, seja do coletor de lixo ou da troca de contexto, vamos primeiro dar uma olhada nos fundamentos da escrita de código para você mesmo no futuro.


Otimização prematura é a raiz de todo o mal.


Você já ouviu isso antes; Otimização prematura é a raiz de todo o mal. Bem, às vezes. Ao escrever software, acredito firmemente em ser:


  1. tão descritivo quanto possível ; você deve tentar narrar as intenções como se estivesse escrevendo uma história.


  2. tão ideal quanto possível ; o que significa que você deve conhecer os fundamentos da linguagem e aplicá-los de acordo.

Tão descritivo quanto possível

Seu código deve expressar intenção, e muito disso está relacionado à maneira como você nomeia métodos e variáveis.


 int[10] array1; // bad int[10] numItems; // better int[10] backPackItems; // great

Apenas pelo nome da variável já é possível inferir a funcionalidade.


Embora numItems seja abstrato, backPackItems informa muito sobre o comportamento esperado.


Ou digamos que você tenha este método:


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return new ArrayList<>(0); } // (...) return listOfVisitedCountries; }

No que diz respeito ao código, isso parece mais ou menos ok.


Podemos fazer melhor? Nós definitivamente podemos!


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return Collections.emptyList(); } // (...) return listOfVisitedCountries; }

Ler Collections.emptyList() é muito mais descritivo do que new ArrayList<>(0);


Imagine que você está lendo o código acima pela primeira vez e se depara com a cláusula guard que verifica se o usuário realmente visitou países. Além disso, imagine que isso esteja enterrado em uma classe longa, lendo Collections.emptyList() é definitivamente mais descritivo do que new ArrayList<>(0) , você também está garantindo que seja imutável, garantindo que o código do cliente não possa modificá-lo.

O melhor possível

Conheça o seu idioma e use-o de acordo. Se você precisar de um double , não há necessidade de envolvê-lo em um objeto Double . O mesmo vale para usar um List se tudo que você realmente precisa é de um Array .


Saiba que você deve concatenar Strings usando StringBuilder ou StringBuffer se estiver compartilhando estado entre threads:


 // don't do this String votesByCounty = ""; for (County county : counties) { votesByCounty += county.toString(); } // do this instead StringBuilder votesByCounty = new StringBuilder(); for (County county : counties) { votesByCounty.append(county.toString()); }


Saiba como indexar seu banco de dados. Antecipe gargalos e armazene o cache de acordo. Todos os itens acima são otimizações. São o tipo de otimizações que vocês devem conhecer e implementar como primeiros cidadãos.

Como você o mata primeiro?

Nunca esquecerei um hack que li alguns anos atrás. Verdade seja dita, o autor voltou atrás rapidamente, mas isso mostra como muito mal pode surgir de boas intenções.


 // do not do this, ever! int i = 0; while (i<10000000) { // business logic if (i % 3000 == 0) { //prevent long gc try { Thread.sleep(0); } catch (Ignored e) { } } }

Um hack de coletor de lixo do inferno!


Você pode ler mais sobre por que e como o código acima funciona no artigo original e, embora a exploração seja definitivamente interessante, essa é uma daquelas coisas que você nunca deve fazer.


  • Funciona por efeitos colaterais, Thread.sleep(0) não tem propósito neste bloco
  • Funciona explorando uma deficiência de código downstream
  • Para quem herda esse código, ele é obscuro e mágico


Só comece a forjar algo um pouco mais complexo se, depois de escrever com todas as otimizações padrão que a linguagem fornece , você encontrar um gargalo. Mas evite misturas como as acima.


Uma interpretação do futuro Garbage Collector do Java “imaginado” pelo Microsoft Copilot


Como lidar com esse coletor de lixo?

Se depois de tudo feito o Coletor de Lixo ainda é a peça que oferece resistência, estas são algumas das coisas que você pode tentar:


  • Se o seu serviço for tão sensível à latência que você não possa permitir o GC, execute com "Epsilon GC" e evite o GC completamente .
    -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC


    Obviamente, isso aumentará sua memória até obter uma exceção OOM; portanto, ou é um cenário de curta duração ou seu programa está otimizado para não criar objetos


  • Se o seu serviço for um pouco sensível à latência, mas a tolerância permitida permitir alguma margem de manobra , execute GC1 e alimente-o com algo como -XX:MaxGCPauseTimeMillis=100 (o padrão é 250ms)

  • Se o problema surgir de bibliotecas externas , digamos que uma delas chame System.gc() ou Runtime.getRuntime().gc() que são coletores de lixo que param o mundo, você pode substituir o comportamento ofensivo executando com -XX:+DisableExplicitGC


  • Se você estiver executando em uma JVM acima de 11, experimente o Z Garbage Collector (ZGC) , as melhorias de desempenho são monumentais! -XX:+UnlockExperimentalVMOptions -XX:+UseZGC . Você também pode querer verificar este benchmark JDK 21 GC .


INÍCIO DA VERSÃO

FIM DA VERSÃO

GC PADRÃO

Java 1

Java4

Coletor de lixo em série

Java 5

Java 8

Coletor de lixo paralelo

Java 9

em andamento

Coletor de Lixo G1


Nota 1: desde o Java 15, ZGC está pronto para produção , mas você ainda precisa ativá-lo explicitamente com -XX:+UseZGC .


Nota 2: A VM considera as máquinas como de classe servidor se a VM detectar mais de dois processadores e um tamanho de heap maior ou igual a 1792 MB. Se não for de classe de servidor, o padrão será Serial GC .


Em essência, opte pelo ajuste do GC quando estiver claro que as restrições de desempenho do aplicativo estão diretamente ligadas ao comportamento da coleta de lixo e você tiver o conhecimento necessário para fazer ajustes informados. Caso contrário, confie nas configurações padrão da JVM e concentre-se na otimização do código no nível do aplicativo.

u/shiphe - você vai querer ler o comentário completo


Outras bibliotecas relevantes que você pode querer explorar:

Chicote de Microbenchmark Java (JMH)

Se você está otimizando por sentimento, sem qualquer benchmarking real, você está prestando um péssimo serviço a si mesmo. JMH é a biblioteca Java de fato para testar o desempenho de seus algoritmos. Use-o.

Java-Thread-Afinidade

Fixar um processo em um núcleo específico pode melhorar os acessos ao cache. Dependerá do hardware subjacente e de como sua rotina está lidando com os dados. No entanto, esta biblioteca torna-a tão fácil de implementar que, se um método com uso intensivo de CPU estiver atrapalhando você, você vai querer testá-lo.

Disruptor LMAX

Essa é uma daquelas bibliotecas que, mesmo que você não precise, vai querer estudar. A ideia é permitir simultaneidade de latência ultrabaixa. Mas a forma como é implementado, desde a simpatia mecânica até o ring buffer, traz muitos conceitos novos. Ainda me lembro de quando o descobri, há sete anos, passando a noite toda para digeri-lo.

Netflix jvmquake

A premissa do jvmquake é que quando as coisas dão errado com a JVM, você quer que ela morra e não trave. Alguns anos atrás, eu estava executando simulações em um cluster HTCondor que estava com restrições de memória restritas e, às vezes, os trabalhos ficavam travados devido a erros de "falta de memória".


Esta biblioteca força a morte da JVM, permitindo que você lide com o erro real. Neste caso específico, o HTCondor reprogramaria automaticamente o trabalho.

Pensamentos finais

O código que me fez escrever este post? Já escrevi muito pior. Eu ainda faço. O melhor que podemos esperar é bagunçar cada vez menos.


Espero ficar descontente ao olhar para meu próprio código daqui a alguns anos.


E isso é um bom sinal.



Edições e obrigado:


Também publicado em wasteofserver.com