Uma pausa de 40 ms no garbage collector do Go causada por swap

Um caso de pausas stop-the-world de 40 ms em Go, atribuídas ao garbage collector tocando metadados que haviam sido movidos para swap, provoca um escrutínio mais amplo de como o GC interage com a pressão de memória do sistema operacional. Comentadores debatem se sistemas de baixa latência devem evitar totalmente linguagens com GC, desativar swap ou usar técnicas como `mlock`, coletores cooperativos ou cientes de swap, e algoritmos alternativos como GC on-the-fly ou reference counting. A discussão destaca um trade-off entre throughput e latência, e sugere que tanto o design do runtime quanto as APIs em nível de SO moldam significativamente o comportamento real das pausas.

Projeto de GC do Go e alternativas “on-the-fly”

  • O GC do Go já usa um GC concorrente de mark-sweep com tempos de pausa típicos muito curtos.
  • Alguns perguntam por que o Go não adota algoritmos clássicos de GC “on-the-fly” (por exemplo, do tipo DLG) que visam pausas zero de stop-the-world (STW).
  • Outros observam que a equipe do Go provavelmente conhece esses algoritmos; trade-offs entre latência e throughput, além da complexidade de varredura de stack/corrotinas, podem explicar as escolhas atuais.
  • O algoritmo alternativo on-the-fly do Fil-C é mencionado como potencialmente relevante para o modelo de corrotinas do Go.

Swap, page faults e picos de latência

  • O problema central: quando metadados do GC ou páginas do heap são removidos para swap, seções STW podem acumular muitas page faults, transformando pressão de memória em grandes picos de latência.
  • Alguns argumentam que, se você se importa com latência, deve desativar o swap; outros contrapõem que isso apenas desloca as paradas para páginas de código e pode provocar OOM em vez disso.
  • São dadas explicações de como o kernel expulsa páginas anônimas versus páginas apoiadas por arquivo; desativar o swap não impede paging, apenas muda o que é expulso.
  • Mitigações sugeridas: mlock/mlockall para regiões críticas ou metadados, uso cuidadoso de cgroups e swappiness, ou aceitar OOM-kill como algo preferível a tempestades de swap.

Reference counting vs GC por tracing

  • Vários participantes afirmam que reference counting (RC) oferece desempenho mais previsível; outros discordam, citando:
    • Desativações em cascata podem durar arbitrariamente muito.
    • Alto overhead, especialmente com RC atômico em programas multithread.
    • Problemas com ciclos e NUMA/travamento.
  • Alguns preferem esquemas híbridos ou RC diferido; outros enfatizam que décadas de experiência da indústria mostram que o RC puro tem trade-offs significativos.

Cooperação entre SO e GC e designs cientes de swap

  • Ideias levantadas:
    • GC ciente de swap que faz prefetch ou trava páginas usadas em fases STW.
    • Uso de APIs existentes como mlock, madvise, mincore ou toque de página para gerenciar residência.
    • Cooperação mais profunda entre SO e GC (por exemplo, bookmark collectors, gerenciamento cooperativo de memória, GCs baseados em regiões coordenando com madvise).
  • Alguns veem isso como uma oportunidade perdida de pesquisa/prática; outros duvidam que uma integração tão profunda apareça amplamente tão cedo.

Escolha de linguagem e requisitos de latência

  • Vários comentários tratam isso como mais um dado: use Go quando a latência ocasional do GC for aceitável; escolha Rust/C++ (RAII/controle manual) para necessidades estritas de baixa latência.
  • O caso do Discord é citado: mesmo com pouco lixo, um grande grafo de objetos mais GC periódico pode causar latência de cauda inaceitável, motivando uma reescrita em uma linguagem sem GC.