O dia em que o minmax.h emagreceu (e o kernel ficou muito mais rápido)

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...

Linux kernel minmax.h: menos macro, mais velocidade.

Se você já abriu o código do kernel, sabe que a header include/linux/minmax.h parece inofensiva: meia dúzia de macros — min(), max(), clamp() — para comparar valores. Só que, ao longo dos anos, essas macros foram crescendo, ganhando verificação de tipos, truques de “const-expr” e camadas de proteção contra armadilhas da linguagem C. Resultado? Um monstro de metaprogramação que explodia o pré-processador em cascata e tornava algumas compilações absurdamente lentas. Este artigo conta como a dupla Linus Torvalds e David Laight liderou uma limpeza profunda e modernização do Linux kernel minmax.h, e por que isso fez tanta diferença — inclusive com um caso real de 16× de speed-up numa linha problemática de driver.

Como chegamos aqui

Para evitar bugs sutis, o kernel fazia as macros min()/max()/clamp() checarem compatibilidade de tipos (principalmente de sinal, assinado vs. não assinado) e ainda tentava manter o resultado como expressão constante quando possível. Isso parecia prudente — até que o custo se acumulou. Cada macro expandia em ramos de __builtin_choose_expr(), detecções de signedness, e duplicações de argumentos para evitar avaliar duas vezes (efeitos colaterais). Agora imagine isso dentro de outras macros que também dependem de min()/max(). A multiplicação explosiva de tokens tornava certas linhas… épicas.

Linus deu um exemplo inesquecível: um arquivo do driver AtomISP tinha uma única linha que, depois das expansões, atingia 23,3 MB de texto de pré-processador (!). Com as mudanças em minmax.h, essa mesma linha caiu para 1,4 MB, e o tempo de compilação daquela unidade despencou de ~16,6 s para ~2,5 s — um ganho de cerca de 16× no caso extremo. Isso está descrito na própria justificativa de commit da simplificação de min()/max()/clamp() (e mesmo num backport para estável 5.10, de tão útil que ficou).

A faxina: menos mágica, mais objetividade

O pacote de alterações veio em ondas, com commits assinados por Linus Torvalds e uma série de refinos por David Laight. O fio condutor foi: remover complexidade desnecessária sem perder segurança de tipos onde realmente importa. Os principais pontos:

  • Separar casos de “constante de topo”: surgiram macros explícitas MIN()/MAX() (em maiúsculas) e MIN_T()/MAX_T() para situações onde queremos avaliar expressões constantes ou inicializadores estáticos sem arrastar toda a parafernália das versões minúsculas. Essas variantes não fazem checagem de tipos e podem avaliar argumentos mais de uma vez — então são para usos óbvios (p. ex., limites fixos). Isso ajuda a evitar que constantes centrais (como pageblock_order) virem “lama” de 2,5 kB toda vez que aparecem dentro de outra macro. (GitLab)
  • Simplificar min_t()/max_t(): como o tipo já é dado, a macro não precisa da mesma bateria de verificações. Linus cortou o suporte a contextos que exigem strict C constant expressions (array sizes, inicializadores) nessas versões tipadas; quando for realmente necessário ser constante, use MIN_T/MAX_T. Menos caminhos especiais = menos tokens expandidos. (GitLab)
  • A grande poda em min()/max()/clamp(): o commit “minmax: simplify min()/max()/clamp() implementation” removeu o caminho de “se for constexpr, faz X; senão, Y”, e passou a sempre usar temporários do tipo certo, com checagem de signedness mais direta. Isso corta toneladas de __builtin_choose_expr() aninhados e diminui radicalmente o tamanho das expansões — é aqui que mora o salto de 23 MB → 1,4 MB e o speed-up de 16× naquele driver da Intel.
  • Checagem de tipos mais esperta (e barata): a verificação de “podemos comparar esses dois valores sem surpresas?” ficou mais clara e menos inflada. Ex.: inteiros assinados estaticamente não negativos podem participar de comparações sem carregar a paranoia toda; tipos não assinados pequenos (promovidos a int) continuam aceitos por compatibilidade. Menos macro-ginástica, mesma segurança prática.
  • min3()/max3() corrigidos: as versões “de três” antes compunham min(min(x,y),z) com casts de typeof(x), o que podia gerar resultados incorretos em combinações de tamanhos e sinais (além de expandir demais). Agora, usam a mesma infraestrutura “careful” de comparação com temporários e checagem única. Adeus armadilhas.
  • clamp() e família: a família toda foi reorganizada (definições agrupadas com min/max) e simplificada. A sanidade lo ≤ hi virou um BUILD_BUG_ON_MSG(statically_true(ulo > uhi)), cobrindo melhor os casos e evitando outra rodada de expansão, e as variantes clamp_t() / clamp_val() foram alinhadas para reaproveitar o mesmo núcleo enxuto. Há até limpeza cosmética (espaços, comentários) — menos ruído, mais leitura.

Efeito dominó no kernel (e nos estáveis)

Não é “apenas” academia de macros. As simplificações liberaram vários trechos do kernel de gambiarras locais: drivers que definiam seus próprios MIN/MAX, usos de max() em locais que precisavam de constante (trocados por MAX()), buffers dimensionados com max_t() convertidos para MAX_T(), e por aí vai. O resultado é um ecossistema mais consistente: menos duplicata, menos colisão de nomes, menos expansão redundante.

Por ser um ganho concreto de tempo de compilação (e de saúde mental para quem depura pré-processamento), a série começou a ser levada para ramos estáveis. Há evidências tanto nos dashboards que rastreiam commits quanto em listas que carregam backports (o texto do commit com os números de 23 MB → 1,4 MB aparece inclusive numa série de 5.10.y). Em resumo: não é só mainline que se beneficia; LTS também.

Por que isso importa (além da velocidade)

O Linux kernel minmax.h é um daqueles alicerces invisíveis. Ele aparece em todo lugar — do VM ao DRM — e qualquer microcusto nele multiplica. Reduzir 10×, 15×, 16× o tamanho de certas expansões significa:

  • Builds mais rápidas e previsíveis, especialmente em módulos “macro-pesados”.
  • Logs de pré-processamento legíveis quando você realmente precisa investigar por que algo virou espaguete.
  • Menos risco de cair em cantos obscuros de promoção implícita de tipos do C — a checagem ficou mais clara sobre o que é permitido (e por quê).
  • Código do kernel mais limpo: standardizar MIN/MAX em maiúsculas para constantes e min/max para o dia a dia reduz “dialetos locais”.

E, num nível cultural, é um lembrete do estilo Torvalds: descobrir um gargalo “besta”, entender o porquê, simplificar — e aceitar um pouco menos de magia quando a simplicidade entrega 99% do benefício sem pedágios.

No fim, o que mudou? Não a funcionalidade — min, max, clamp continuam fazendo o óbvio —, mas como chegam lá: com menos alquimia macro e mais eficiência prática. E isso, num projeto do tamanho do kernel, faz toda a diferença.

Compartilhe este artigo