O Linux Kernel pode ficar mais leve justamente onde ninguém espera: nas tarefas mais simples. Uma série RFC enviada por Gabriel Krisman Bertazi (SUSE) propõe otimizar a inicialização e o teardown dos contadores rss_stat usando uma ideia bem direta: parar de montar uma estrutura cara de percpu_counter para processos single-threaded que jamais vão se beneficiar da escalabilidade per-CPU. O nome que resume a abordagem é Linux kernel lazy counters.
A analogia da “mesa de jantar” captura o espírito da proposta. Imagine um restaurante em que, toda vez que você vai jantar sozinho, ele é obrigado a montar uma mesa de banquete para 256 pessoas, só para garantir. Isso custa tempo e trabalho. O patchset faz o óbvio: monta uma mesa para um (contador simples) e, se os convidados chegarem (threads), aí sim expande para o banquete (contador per-CPU).
O custo oculto do fork em sistemas grandes
O problema cresce com o número de CPUs. Em máquinas com dezenas ou centenas de núcleos, a alocação de memória per-CPU necessária para inicializar um percpu_counter vira um overhead real no caminho do fork. Isso é especialmente visível em workloads que criam processos curtos em massa, como comandos do shell, ferramentas de coreutils e loops de execução que chamam binários minimalistas.
Esse ponto não é teórico. Jan Kara já havia relatado anteriormente uma regressão de cerca de 10% no tempo de sistema em gitsource após a introdução de contadores per-CPU para rss_stat. O que o RFC tenta corrigir é justamente essa “taxa fixa” que o sistema cobra mesmo quando o processo não tem contenda entre CPUs e nem atualizações frequentes de múltiplos contextos.
A solução: contadores “preguiçosos” (lazy) com dois modos
O coração da série é uma nova estrutura de contador “bi-modal”. Ela começa barata e só vira cara quando a realidade exige.
- Modo lazy (barato): atualizações locais usam aritmética simples em uma variável. Atualizações “não locais” usam um caminho atomic, pensado para situações raras em tarefas single-threaded, como alguns slow paths associados a OOM e rotinas como khugepaged.
- Modo per-CPU (caro, escalável): quando as atualizações remotas passam a ser frequentes, o contador pode sofrer um “upgrade” e passar a se comportar como um percpu_counter completo.
Se quiser outra analogia além da mesa: é como começar anotando gastos numa caderneta (rápido e barato). Quando a família inteira passa a registrar despesas ao mesmo tempo, você migra para uma planilha compartilhada com abas por pessoa (mais complexa, mas escalável).
Como funciona: upgrade quando o mm_struct é compartilhado
O patchset aplica essa API diretamente nos contadores rss_stat do mm_struct. O ponto inteligente é decidir quando promover o contador.
A regra prática é: força-se o upgrade assim que uma segunda tarefa passa a compartilhar o mesmo mm_struct. Em outras palavras, quando o processo deixa de ser “sozinho” e passa a ser efetivamente multi-threaded, tipicamente em cenários que envolvem CLONE_VM ou criação de threads. Com isso, o custo pesado sai do primeiro fork (o caso comum em processos curtos) e vai para o momento em que o espaço de memória passa a ser compartilhado.
A série também inclui um ajuste bem kernel-hacker: separar o caminho de atualização “local” do caminho “other”. Em pontos do código em que já se sabe que a atualização não vem do contexto local, não faz sentido nem tocar current para checar current->mm. Esse split remove um custo pequeno, mas recorrente, e ajuda a manter o hot path enxuto.
Resultados: ganhos reais, com perfil mostrando onde o tempo foi embora
Os números reportados são típicos de micro-otimização que realmente acertou o alvo:
- Em um microbenchmark de fork chamando
/bin/trueem loop (com afinidade para reduzir migrações), o ganho foi de 6% em uma máquina de 80 cores e de 11% em uma máquina de 256 cores (tempo de sistema). - O perfil em 256 cores mostra o “porquê”:
mm_initcai de 13,5% das amostras para menos de 3,33%, exatamente por reduzir o custo ligado a alocação per-CPU e inicialização de contadores. - Em workload real, o patchset mostra cerca de 1,4% de melhora no tempo de sistema no kernbench (256 cores) e por volta de 3,12% em gitsource.
- E a pergunta inevitável, “isso piora o caso multi-threaded?”, também foi endereçada: em um microbenchmark de 2 threads (“parallel-true”), o custo do upgrade aparece pouco no perfil, e o resultado ainda fica ligeiramente melhor que o baseline (2% a 4%).
O debate na LKML: especialização vs. conserto estrutural
Como todo bom RFC, já apareceu contraponto técnico. Mathieu Desnoyers concorda com o diagnóstico sobre o custo de alocação per-CPU, mas sugere atacar a raiz do problema: hoje o mm_struct soma múltiplas alocações per-CPU separadas (por exemplo mm_cid, futex_ref e os próprios rss_stats). A proposta dele é unificar isso em uma mm_percpu_struct para reduzir o número de alocações e, depois, criar um cache dessa estrutura para desviar do alocador per-CPU. A leitura implícita é interessante: talvez boa parte do ganho venha apenas de arrumar layout e caching, sem precisar de um caso especial para single-threaded.
Esse tipo de troca é justamente o ponto de um RFC: medir, comparar caminhos e decidir se a solução final deve ser um “lazy” elegante, uma reestruturação do per-CPU no mm, ou uma combinação dos dois.
