Go 1.24 troca a implementação interna de maps por Swiss Tables
A mudança mantém a API igual (map[K]V, make, range), mas reorganiza lookup e insert em torno de metadados compactos e melhor localidade de cache. Entenda de onde vem o ganho.
Maps estão no caminho quente de quase todo programa Go não trivial: roteamento de requisições, cache, deduplicação, agregações. Por isso, mesmo melhorias pequenas no runtime rendem ganhos mensuráveis em sistemas reais. Segundo análise publicada por Gabor Koos, o Go 1.24 trouxe uma das maiores mudanças nos internals de maps em anos: o clássico design de buckets com overflow deu lugar a uma implementação inspirada em Swiss Tables.
O ponto importante para quem escreve Go no Brasil: a API externa não mudou. Seu código continua usando map[K]V, make, indexação, delete e range exatamente como antes. A reorganização acontece só por baixo do capô.
Como era o map antigo (pré-1.24)
Antes do 1.24, cada map tinha um array de buckets, cada bucket com até 8 pares chave/valor mais metadados. Quando um bucket enchia, o runtime alocava um bucket de overflow e o encadeava ao anterior. A busca funcionava assim: hash da chave, escolhe o bucket, varre as entradas e, se não achar, caminha pela corrente de overflow.
O design era estável e testado, com crescimento incremental que evitava picos grandes de latência no resize. O custo principal era localidade de memória sob pressão: correntes de overflow significam pointer chasing, e pointer chasing significa cache miss. O limite prático de load factor ficava em torno de 81% (cerca de 6,5 slots preenchidos por bucket de 8) antes que a pressão por crescimento pesasse.
O que as Swiss Tables mudam
As Swiss Tables vieram do trabalho interno de performance do Google e foram documentadas e abertas via Abseil como flat_hash_map. A ideia central: reorganizar o caminho de dados em torno de metadados compactos por slot e grupos contíguos amigáveis a probing.
A sacada é que o probe não começa lendo chaves completas. Ele começa lendo bytes de metadado, baratos de varrer em bloco. A chave é dividida em duas partes lógicas:
h1: escolhe o grupo inicial;h2: um fingerprint curto guardado no metadado de cada slot.
Se o fingerprint de um slot não bate, não há razão para tocar nos bytes da chave. No design do Go, os slots são organizados em grupos fixos de 8, com uma control word (um byte de metadado por slot, empacotados), 8 chaves e 8 valores. Como os 8 bytes de controle são contíguos, o runtime inspeciona o estado de todo o grupo em uma operação apertada.
O lookup vira um loop de dois estágios: lê os bytes de controle, encontra posições cujo fingerprint casa com h2, compara as chaves reais só nessas posições, e avança para o próximo grupo se não houver match. Um slot vazio prova que a chave não existe e encerra o probe.
Isso costuma ser descrito como "SIMD-friendly", mas o ganho maior é localidade e comportamento de branch: varredura contígua de metadados, poucas cargas de chave completa, loop previsível e melhor residência em cache. O load factor útil sobe para a faixa alta dos 80% (frequentemente citado em torno de 87,5% para grupos de 8 slots), o que reduz overhead por entrada e eventos de crescimento.
Adaptações específicas do Go
Não foi um port direto do Abseil. O Go tem restrições de GC, latência e semântica de iteração. Entre as adaptações citadas na fonte:
- Crescimento incremental, evitando rehash monolítico que criaria caudas longas de latência em caminhos quentes;
- Múltiplas tabelas menores com roteamento por diretório (conceitualmente similar a extendible hashing): só o segmento sob pressão precisa dividir;
- Iteração segura durante splits e mudanças de layout, acoplando o estado do iterador a versionamento interno;
- Integração com GC e write barriers, já que chaves e valores podem conter ponteiros.
Os números (e as ressalvas)
Aqui vale calibrar expectativas. As release notes do Go 1.24 relatam redução de CPU de cerca de 2 a 3% em média numa suíte representativa, mas esse é um número de runtime inteiro, não só de map.
Para maps especificamente, o comentário de encerramento de Michael Pratt na issue de acompanhamento (golang/go#54766) reporta acesso e atribuição em mapas grandes cerca de 30 a 35% mais rápidos, iteração ~10% mais rápida no geral (e até ~60% em mapas grandes de baixa carga), enquanto os benchmarks de aplicação completa da suíte Sweet mostraram ganho de média geométrica em torno de 1,5%. Essa distância entre "30% aqui" e "1,5% ali" é esperada: serviços reais gastam tempo em parsing, syscalls, RPC, scheduling e GC.
O lado cauteloso também importa. A issue de cold-cache (golang/go#70835) documenta que indireção por diretório e múltiplas alocações podem aumentar o custo de miss; um relato de benchmarking do Prometheus mostrou mais CPU em runtime.mapaccess1_fast64 no Go 1.24.2 versus 1.23 para aquele perfil. Já a issue golang/go#70617 mostra que clear(m) em mapas grandes e esparsos pode ter custo proporcional ao tamanho alocado.
O que fazer na prática
Se seu serviço é pesado em lookup ou insert com mapas médios a grandes, o Go 1.24+ tende a dar um speedup de graça e, muitas vezes, melhor eficiência de memória. Iteração sobre mapas grandes e esparsos também melhora, já que o runtime pula grupos vazios rápido. Se o seu perfil é dominado por misses frios, mapas enormes e esparsos, ou loops apertados de clear/reuso, meça direto no seu workload: essas são áreas de otimização ainda ativas, não história encerrada.
Fonte: Hacker News
Este artigo foi escrito por Redação iMasters, um agente de inteligência artificial com revisão editorial humana.




