GoSuda

A pausa para um chá verde roubada pelo runtime Go, GreenTea GC

By snowmerak
views ...

Por Que Mais um Novo GC Está Sendo Lançado?

Por Que o GC Existente?

O Garbage Collector (GC) atual do Go pode ser descrito com as seguintes características:

  • Marcação e Varrimento Tri-Color Concorrente (concurrent tri-color mark and sweep)
    • Utiliza 3 cores (branco, cinza e preto) para rastrear o estado dos objetos.
      • Branco: Objeto ainda não visitado.
      • Cinza: Objeto visitado, mas cujos objetos filhos ainda não foram todos visitados.
      • Preto: Objeto cujos todos os objetos filhos foram visitados.
      • Após a conclusão da varredura, todos os objetos brancos são coletados.
    • Utiliza Write Barrier para proteger a memória recém-criada durante o GC.
      • O tempo gasto ativando/desativando o Write Barrier constitui uma parte significativa do que é comumente chamado de Stop-The-World (doravante STW).
    • Este processo é executado de forma concorrente.
  • Sem Compactação (No Compaction)
    • O GC do Go não realiza compactação.
    • Pode ocorrer fragmentação de memória.
      • No entanto, objetos menores que 32kb minimizam a fragmentação utilizando o cache per-P do alocador de memória (Allocator) da linguagem Go.
  • Não Geracional (Non-Generational)
    • O GC do Go não gerencia objetos por gerações.
    • Todos os objetos pertencem à mesma geração.
  • Análise de Escape (Escape Analysis)
    • Go determina se um objeto será alocado no heap ou na stack por meio da Análise de Escape.
    • Em linhas gerais, se houver o uso de ponteiros pendentes (dangling pointers) ou interfaces, o objeto tende a ser alocado no heap.

O Ponto Crucial

O GC do Go realiza uma varredura a partir das raízes (roots) e executa a marcação tri-color em todos os objetos. Este processo pode ser resumido como um algoritmo de busca concorrente em grafo de ordenação topológica. No entanto, é altamente provável que cada objeto resida em diferentes regiões de memória. Para ilustrar, consideremos o seguinte:

  1. Suponha que existam duas regiões de memória separadas por aproximadamente 32MB.
  2. 5 objetos estão alocados em cada uma dessas duas regiões de memória.
    1. Objetos A, B, C na região 1.
    2. Objetos D, E na região 2.
  3. A ordem de referência dos objetos é A -> D -> B -> E -> C.
  4. Quando o GC começa, ele visita A, depois D, depois B, depois E e, por fim, C.
  5. Neste momento, como A e D residem em regiões de memória distintas, o processo de visita de A para D requer a movimentação entre regiões de memória.
  6. Este processo incorre em um custo, conhecido como custo de "salto de memória" (memory jump), que envolve a movimentação entre regiões de memória e o preenchimento de novas linhas de cache. Uma parte significativa do tempo de CPU utilizado pela aplicação pode ser alocada para esta operação do GC.
  7. Este custo ocorre continuamente enquanto o GC está em execução.

Então, Qual é o Problema?

O comportamento atual do GC é desvantajoso nos seguintes cenários:

  • Grande número de Cores e Grande Volume de Memória
    • O GC do Go é fundamentalmente executado de forma concorrente.
    • No entanto, se a região de memória for ampla e os objetos estiverem dispersos, múltiplos cores buscarão objetos simultaneamente em diversos espaços de memória.
    • Se o barramento entre a CPU e a memória não for suficientemente robusto neste processo, a largura de banda da memória pode se tornar um gargalo.
    • Além disso, devido à distância física real, cada busca individual resultará em um atraso comparativamente maior.
  • Grande Número de Objetos Pequenos
    • Se você estiver operando árvores ou grafos com grande profundidade (depth) ou muitos filhos em Go, o GC precisa inspecionar todos esses objetos.
    • Se os objetos estiverem dispersos em diferentes regiões de memória, o atraso ocorrerá devido à primeira razão mencionada.
    • Adicionalmente, se houver muitos objetos pequenos, o GC precisa inspecionar todos eles, o que fará com que os cores da CPU aloquem e executem uma quantidade substancial de capacidade para o GC durante sua execução.

Para resolver esses problemas, a equipe do Golang anunciou o GreenTea GC.

Você Não Terá Mais o Luxo de Beber Chá Verde

O Que Tira o Chá Verde

A área identificada como a que poderia aplicar a solução mais rápida para o GC existente parece ser a localidade da memória (memory locality). Ou seja, minimizar o salto de memória garantindo que os objetos estejam localizados próximos uns dos outros. Contudo, não é possível forçar o padrão de codificação do programador, e a alocação de objetos é imprevisível dependendo do fluxo de trabalho.

Por esta razão, o método escolhido pela equipe do Golang é o Memory Span.

O Que É um Memory Span?

Um memory span é uma região de memória comparativamente grande alocada para alocar objetos pequenos. O novo GC, denominado GreenTea GC, realiza a coleta de lixo especificamente nesses memory spans. O comportamento detalhado é quase idêntico ao Tri-Color Mark and Sweep existente.

O Funcionamento do GreenTea GC

Primeiramente, o GreenTea GC aloca um memory span. O tamanho, conforme mencionado anteriormente, é de 8KiB, que é um tamanho considerável. Dentro dele, podem ser alocados objetos com tamanho máximo de 512 bytes. Este tamanho é escolhido de modo que seria difícil para um nó de árvore ou grafo, ou o tamanho de uma struct típica que escapa para o heap, ser maior do que isso. Objetos menores que 512 bytes são acumulados neste memory span sempre que escapam para o heap; quando o memory span estiver cheio, um novo é alocado.

Quando o GC ocorre, o GreenTea GC enfileira esses memory spans e os inspeciona sequencialmente. Neste processo, o GreenTea GC utiliza um escalonamento quase idêntico ao modelo GMP existente. Funcionalidades como roubo de trabalho (work stealing) também são implementadas. Em todo caso, o worker que retira um memory span da fila inspeciona os objetos internos desse memory span que lhe foi atribuído. O Tri-Color Mark and Sweep é usado de forma idêntica neste processo.

Quais São os Benefícios?

A principal diferença entre este processo e o GC existente é que a unidade de execução do GC mudou de objeto para memory span. Consequentemente, o GC obtém as seguintes vantagens:

  • Localidade da Memória: Como os objetos estão agrupados no memory span, o salto de memória é minimizado ao buscar objetos. Isso permite o máximo aproveitamento do cache da CPU.
  • Melhoria no Desempenho do GC: Ao executar o GC na unidade de memory span, o GC opera de forma mais eficiente em ambientes multi-core.
  • Otimização da Alocação de Memória: Como o memory span é alocado com um tamanho fixo, o overhead de alocação de memória para objetos pequenos é reduzido, e a probabilidade de fragmentação diminui. Isso aumenta a eficiência da desalocação de memória.

Em resumo, agora podemos alocar objetos menores que 512 bytes com mais frequência e com maior tranquilidade.

No entanto, há cenários onde isso é particularmente vantajoso:

  • Estruturas de Árvore/Grafo: Casos em que o fan-out é alto e as alterações são raras.
    • B+ tree, Trie, AST (Abstract Syntax Tree)
    • Estruturas de dados amigáveis ao cache (cache-friendly)
  • Processamento em Lote: Fluxos de trabalho de alocação em massa de pequenos dados.
    • Múltiplos objetos pequenos criados por parsing JSON.
    • Objetos DAO (Data Access Object) de conjuntos de resultados de banco de dados.

Nesses casos, o GreenTea GC pode apresentar um desempenho superior ao GC existente. Contudo, ele não se aplica a todos os casos, e ainda é difícil esperar melhorias dramáticas de desempenho, especialmente quando os objetos estão dispersos em diferentes regiões de memória.

Conclusão

A equipe do Golang parece ter a intenção de continuar aprimorando o GC a longo prazo. Este GreenTea GC pode ser visto como uma mudança menor em uma área específica e não substitui o GC existente. No entanto, o GreenTea GC é um caso interessante que permite vislumbrar os problemas que a equipe do Golang enfrenta ou antecipa. A tentativa de resolver um problema surpreendentemente complexo com a adição de um conceito relativamente simples também foi impressionante. Pessoalmente, considero este um caso fascinante.

O GreenTea GC será introduzido como um recurso experimental a partir do Go 1.25. Ele pode ser ativado usando a variável de ambiente GOEXPERIMENT=greenteagc. Como este recurso ainda é experimental, testes suficientes são necessários antes de ser usado em um ambiente de produção.

Referências