O Linux pode acelerar tarefas simples em até 15% com contadores “preguiçosos”

RFC da SUSE cria contadores preguiçosos para rss_stat e reduz o custo de percpu_counter no fork de processos simples.

Escrito por
Emanuel Negromonte
Emanuel Negromonte é Jornalista, Mestre em Tecnologia da Informação e atualmente cursa a segunda graduação em Engenharia de Software. Com 14 anos de experiência escrevendo sobre...

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/true em 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_init cai 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.

Compartilhe este artigo