A pausa para um chá verde roubada pelo runtime Go, GreenTea GC
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).
- O tempo gasto ativando/desativando o
- Este processo é executado de forma concorrente.
- Utiliza 3 cores (branco, cinza e preto) para rastrear o estado dos objetos.
- 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.
- No entanto, objetos menores que 32kb minimizam a fragmentação utilizando o cache
- 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:
- Suponha que existam duas regiões de memória separadas por aproximadamente 32MB.
- 5 objetos estão alocados em cada uma dessas duas regiões de memória.
- Objetos A, B, C na região 1.
- Objetos D, E na região 2.
- A ordem de referência dos objetos é
A -> D -> B -> E -> C
. - Quando o GC começa, ele visita A, depois D, depois B, depois E e, por fim, C.
- 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.
- 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. - 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.
- Se você estiver operando árvores ou grafos com grande profundidade (
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.