A xícara de chá verde de tempo livre que o runtime do Go tomará de você: GreenTea GC

Por que surge outro GC novo
Por que o GC existente
O GC atual do Go pode ser descrito pelos seguintes termos.
- concurrent tri-color mark and sweep
- Utiliza um total de 3 cores (branco, cinza e preto) para acompanhar 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 objetos filhos foram todos visitados
- Ao término da travessia, todos os objetos brancos são coletados.
- Utiliza uma Write Barrier para proteger a memória criada durante o GC.
- O tempo de ativação e desativação da Write Barrier corresponde a uma parte significativa do que comumente se chama Stop-The-World (doravante STW).
- Este processo é executado de forma concorrente.
- Utiliza um total de 3 cores (branco, cinza e preto) para acompanhar o estado dos objetos.
- No Compaction
- O GC do Go não realiza compactação.
- Pode ocorrer fragmentação de memória.
- No entanto, objetos de até 32 KB utilizam o cache per-P do alocador de memória (Allocator) da linguagem Go, minimizando a fragmentação.
- Non-Generational
- O GC do Go não gerencia os objetos por gerações.
- Todos os objetos pertencem à mesma geração.
- Escape Analysis
- O Go determina, por meio da Escape Analysis, se um objeto é alocado no heap ou na stack.
- De forma aproximada, pode-se considerar que, se houver ponteiros pendentes (dangling pointers) ou o uso de interfaces, o objeto é alocado no heap.
O que é importante
O GC do Go percorre todos os objetos a partir da raiz e executa a marcação tricolor. Esse processo pode ser resumido em uma linha como algoritmo de travessia concorrente de grafo com ordenação topológica. Contudo, cada objeto provavelmente está em regiões de memória distintas. Em termos simples:
- Suponha que existam duas regiões de memória separadas por cerca de 32 MB.
- Cada uma dessas regiões de memória contém 5 objetos alocados.
- Os objetos A, B e C estão na região 1.
- Os objetos D e E estão na região 2.
- A ordem de referências entre os objetos é
A -> D -> B -> E -> C. - Quando o GC começa, ele visita A, depois D, depois B, depois E e, por fim, C.
- Nesse caso, como A e D estão em regiões de memória diferentes, é necessário trocar de região de memória ao visitar D após A.
- Nesse processo, ocorre a chamada penalidade de salto de memória (memory jump), que envolve a troca de região de memória e o preenchimento de novas linhas de cache. Uma parcela significativa do tempo de CPU utilizado pela aplicação pode ser destinada a essas operações de GC.
- Esse custo continua a ocorrer durante todo o período em que o GC estiver em execução.
Então, qual é o problema?
O comportamento desse GC é fatal nos seguintes casos.
- Quando há muitos núcleos e a memória é grande
- O GC do Go é executado, por padrão, de forma concorrente.
- Entretanto, quando a região de memória é ampla e os objetos estão dispersos, vários núcleos passam a percorrer objetos em diversos espaços de memória simultaneamente.
- Nesse processo, se o barramento entre a CPU e a memória não for suficientemente largo, a largura de banda de memória pode se tornar um gargalo.
- Além disso, por haver distância física real, cada travessia pode introduzir um atraso relativamente grande.
- Quando há muitos objetos pequenos
- Se você opera árvores ou grafos com profundidade grande ou muitos filhos em Go, o GC precisa percorrer todos esses objetos.
- Nesse processo, se os objetos estiverem dispersos em diferentes regiões de memória, ocorre atraso pelo primeiro motivo mencionado.
- Além disso, havendo muitos objetos pequenos, o GC precisa percorrer todos eles, de modo que, durante a execução do GC, os núcleos da CPU dedicam uma quantidade considerável de recursos ao GC e ao trabalho que ele realiza.
Para resolver esses problemas, a equipe do Golang apresentou o GreenTea GC.
Você não tem mais tempo para tomar chá verde
O que tira o chá verde
Parece que a área na qual a equipe considerou possível aplicar a solução mais rápida para o GC existente foi a localidade de memória. Ou seja, trata-se de posicionar os objetos próximos uns dos outros para minimizar os saltos de memória. Contudo, não é possível impor padrões aos programadores que escrevem o código e, além disso, a alocação de objetos é imprevisível de acordo com o fluxo de trabalho.
Talvez por isso, a abordagem escolhida pela equipe do Golang tenha sido o span de memória (Memory Span).
O que é um span de memória?
Um span de memória é uma área que recebe uma alocação de memória relativamente grande e nela aloca objetos pequenos. E o novo GC, denominado GreenTea GC, executa a coleta de lixo sobre esses spans de memória. Os detalhes de operação são quase idênticos ao tradicional Tri-Color Mark and Sweep.
Funcionamento do GreenTea GC
Primeiramente, o GreenTea GC aloca um span de memória. Seu tamanho, conforme descrito anteriormente, é de 8 KiB, um tamanho considerável. Nele, podem ser alocados objetos de até 512 bytes. Esse é aproximadamente o tamanho máximo que os nós de árvores ou grafos mencionados como exemplo, ou as estruturas comuns que escapam para o heap, costumam ter. Objetos de até 512 bytes são acumulados nesse span de memória sempre que escapam para o heap, e, quando o span fica cheio, um novo span de memória é alocado.
Quando ocorre um GC, o GreenTea GC enfileira esses spans de memória e os inspeciona sequencialmente. Nesse processo, o GreenTea GC utiliza um escalonamento quase semelhante ao modelo GMP existente. Também estão implementados mecanismos como o roubo de trabalho (work stealing). De qualquer forma, o worker que retira um span de memória da fila inspeciona os objetos internos do span que lhe foi atribuído. Nesse processo, o Tri-Color Mark and Sweep é utilizado da mesma forma.
Qual é a vantagem?
Considerando o processo como um todo, a diferença em relação ao GC existente resume-se, em grandes linhas, a um único ponto: a unidade de execução do GC passou de objeto para span de memória. Por causa disso, o GC apresenta as seguintes vantagens.
- Localidade de memória: como os objetos estão agrupados no span de memória, os saltos de memória são minimizados ao percorrer os objetos. Ou seja, é possível aproveitar ao máximo o cache da CPU.
- Melhoria de desempenho do GC: ao executar o GC na unidade de span de memória, o GC passa a operar de forma mais eficiente em ambientes multicore.
- Otimização da alocação de memória: como os spans de memória são alocados com tamanho fixo, o overhead de alocação ao criar objetos pequenos diminui, e a possibilidade de fragmentação se reduz. Isso aumenta a eficiência da liberação de memória alocada.
Em resumo, a partir de agora podemos alocar objetos de até 512 bytes com mais frequência e com mais tranquilidade.
Contudo, também existem certos cenários nos quais essa abordagem é vantajosa.
- Estruturas de árvore/grafo: casos com alto fan-out e poucas alterações
- B+ trees, Tries, AST (Abstract Syntax Tree)
- Estruturas de dados amigáveis ao cache
- Processamento em lote: fluxos de trabalho com alocação em grande volume de dados pequenos
- Múltiplos objetos pequenos gerados pelo parsing de JSON
- Objetos DAO de conjuntos de resultados de banco de dados
Nesses casos, o GreenTea GC pode apresentar desempenho superior ao GC existente. No entanto, isso não se aplica a todas as situações e, especialmente quando os objetos estão dispersos em diferentes regiões de memória, ainda é difícil esperar melhorias de desempenho dramáticas.
Então
Parece que a equipe do Golang pretende continuar aprimorando o GC a longo prazo. Este GreenTea GC pode ser visto como uma mudança pequena em uma área restrita e não substitui o GC existente. Contudo, o GreenTea GC é um caso interessante que permite vislumbrar os problemas que a equipe do Golang enfrenta ou prevê. Embora seja um problema complexo, foi impressionante a tentativa de resolvê-lo pela adição de um conceito relativamente simples. Pessoalmente, considero-o um caso interessante.
O GreenTea GC é um recurso experimental introduzido a partir do Go 1.25. Ele pode ser ativado utilizando a variável de ambiente GOEXPERIMENT=greenteagc. Como este recurso ainda é experimental, são necessários testes suficientes antes de utilizá-lo em ambientes de produção.