Desenvolvimento

    Arquitetura NUMA, Afinidade de CPU e Hierarquia de Memória: Engenharia de Baixa Latência em Runtimes Multithreaded

    Entenda a física da arquitetura NUMA, as penalidades de latência do barramento de interconexão e as técnicas de CPU pinning e alocação first-touch para eliminar jitter em runtimes de alta concorrência.

    2026-09-0517 minEquipe MaxVision
    CLIP_001 · DJI O4FPV · 4K · 60FPS
    DESENVOLVIMENTO · 2026.09.05

    A memória RAM em servidores modernos não é uniforme nem homogênea. Em nós multissocket ou processadores modulares baseados em chiplets, acessar memória vinculada a outro nó custa entre 1,5× e 2,5× mais tempo do que acessar a memória DRAM local.

    Essa discrepância física transforma o modelo clássico de multiprocessamento simétrico em um gargalo silencioso.

    Sistemas que ignoram a topologia de hardware sofrem quedas severas de vazão e picos imprevisíveis de latência de cauda (tail latency p99).

    Interior de servidor de alta densidade em rack sob iluminação chiaroscuro com soquetes de processadores interligados por barramentos e cabos de fibra ótica com travas e guia de sinalização industrial visível em primeiro plano

    Como o fim da memória uniforme redefiniu a microarquitetura de servidores

    A memória uniforme ruiu quando o aumento de núcleos superou a capacidade física do barramento frontal compartilhado (Front Side Bus). Em arquiteturas contemporâneas, cada soquete ou grupo de núcleos possui canais de memória DRAM dedicados diretamente integrados ao silício.

    No modelo SMP (Symmetric Multiprocessing) tradicional, todos os núcleos disputavam o mesmo controlador de memória.

    Essa contenção elétrica limitava a escalabilidade dos servidores a poucos processadores.

    Para romper esse limite físico, a indústria adotou a arquitetura NUMA (Non-Uniform Memory Access). Cada grupo de núcleos passou a formar um nó independente com seus próprios controladores integrados.

    Processadores como AMD EPYC e Intel Xeon Scalable fragmentaram a memória em domínios locais.

    Métrica de Acesso à MemóriaAcesso Local (Mesmo Nó NUMA)Acesso Remoto (Nó NUMA Vizinho)Penalidade Relativa
    Latência de Leitura DRAM60 ns a 80 ns140 ns a 210 ns2,3× a 2,6× mais lento
    Largura de Banda de Pico350 a 460 GB/s por soqueteLimitada pelo link (64 a 128 GB/s)Queda de até 70% na vazão
    Coerência de Cache (RFO)Resolução local em cache L3Broadcast inter-socket via UPI/IFISTráfego intenso de snooping
    Consumo de Energia por Byte~3 a 5 pJ/bit~15 a 25 pJ/bit3× a 5× mais energia gasta

    Qual é o impacto real da latência de interconexão no pipeline da CPU

    A travessia de barramentos de interconexão paralisa o pipeline da CPU por centenas de ciclos de clock adicionais durante faltas de cache L3. Quando um núcleo solicita dados alocados na memória de outro nó, o controlador precisa emitir mensagens de coerência através dos links físicos.

    Conforme demonstrado por Ulrich Drepper em seu clássico ensaio What Every Programmer Should Know About Memory, a cache L1 entrega dados em 4 a 5 ciclos (cerca de 1 ns).

    A memória cache L2 responde em cerca de 14 ciclos, e a cache L3 compartilhada opera em torno de 40 a 50 ciclos.

    Quando ocorre uma perda de cache (cache miss) que demanda leitura remota, o atraso salta para mais de 500 ciclos de CPU.

    O Reorder Buffer (ROB) esgota suas entradas especulativas antes que os dados cheguem da DRAM distante. O pipeline superescalar entra em bolha estrutural (pipeline stall).

    Além da latência crua, operações de escrita que exigem posse exclusiva da linha de 64 bytes (Read For Ownership) saturam os filtros de coerência.

    Bancada de laboratório de hardware com processador multi-die aberto sobre placa de teste com microtrilhas de interconexão iluminadas por luminária de inspeção articulada com refletor metálico em primeiro plano

    Por que a armadilha do first-touch no Linux derruba o paralelismo de aplicações

    O kernel do Linux aloca páginas físicas de memória apenas no primeiro acesso de escrita, e não no momento da reserva virtual. Essa política faz com que estruturas inicializadas por uma única thread fiquem concentradas no mesmo nó NUMA.

    Quando uma aplicação executa chamadas como malloc ou mmap, o sistema operacional reserva apenas espaço de endereçamento virtual.

    Nenhuma página física de 4 KB ou 2 MB é mapeada na tabela de páginas (page table) nesse instante inicial.

    A atribuição da memória física ocorre no manipulador de falha de página (page fault handler) do kernel, na função do_anonymous_page.

    O kernel vincula a página física ao nó NUMA onde reside a thread que executou a primeira escrita.

    Esse comportamento padrão do Linux, documentado nas Políticas de Memória NUMA do Kernel, cria a armadilha da inicialização monothread (Single-Thread Init Trap).

    Se a thread principal zera um grande vetor de dados antes de despachar tarefas para as threads de trabalho, toda a memória física residirá em um único nó.

    Quando as outras threads iniciam o processamento em núcleos remotos, todas as suas leituras e gravações cruzarão o barramento de interconexão.

    // ARMADILHA: Toda a memória fica alocada no nó NUMA da thread principal
    double *buffer = (double *)malloc(N * sizeof(double));
    #pragma omp parallel for
    for (size_t i = 0; i < N; i++) {
        buffer[i] = 0.0; // Incorreto se executado antes da fixação das threads
    }
    
    // CORRETO: Cada thread de trabalho toca e inicializa sua própria fatia local
    #pragma omp parallel
    {
        int tid = omp_get_thread_num();
        size_t chunk = N / omp_get_num_threads();
        size_t start = tid * chunk;
        size_t end = (tid == omp_get_num_threads() - 1) ? N : start + chunk;
    
        for (size_t i = start; i < end; i++) {
            buffer[i] = 0.0; // Mapeia páginas físicas no nó NUMA de cada worker
        }
    }
    

    Como configurar afinidade de CPU e políticas de alocação no Linux

    O controle determinístico de afinidade exige vincular processos e threads a núcleos específicos e definir políticas estritas de nós de memória. Ferramentas como numactl e chamadas de sistema da biblioteca libnuma de Andi Kleen eliminam a migração estocástica de processos.

    Mover uma thread ativa entre nós NUMA descarta a localidade temporal das caches L1, L2 e L3 locais.

    Para evitar essa migração indesejada, engenheiros utilizam a chamada de sistema sched_setaffinity ou pthread_setaffinity_np.

    No gerenciamento de memória, o kernel disponibiliza políticas distintas via chamada mbind:

    • MPOL_BIND: Restringe as alocações estritamente aos nós especificados, falhando se a memória local esgotar.
    • MPOL_PREFERRED: Prioriza a alocação no nó local, recorrendo a nós remotos apenas sob pressão severa de RAM.
    • MPOL_INTERLEAVE: Alterna páginas de memória em round-robin entre múltiplos nós NUMA para maximizar a largura de banda agregada.
    • MPOL_LOCAL: Aloca páginas no nó da thread que disparou o acesso, mantendo a regra natural de first-touch.

    Aplicações orientadas a ultra baixa latência utilizam o comando numactl para amarrar tanto o conjunto de CPUs quanto a memória:

    # Executa o binário fixado nos núcleos 0 a 15 com memória estritamente no nó 0
    numactl --cpunodebind=0 --membind=0 ./motor_alta_frequencia --config=prod.json
    
    # Inspeciona as distâncias relativas entre nós NUMA na tabela ACPI SLIT
    numactl --hardware
    

    A matriz de distâncias exposta pelo hardware via ACPI SLIT (System Locality Information Table) revela os custos relativos normalizados. O valor 10 representa o nó local, enquanto nós vizinhos apresentam distâncias típicas de 16, 24 ou 32.

    Por que runtimes com work-stealing global colapsam em nós NUMA múltiplos

    Mecanismos globais de roubo de trabalho (work-stealing) transferem tarefas entre núcleos sem considerar a topologia física de caches e nós. Quando um núcleo ocioso rouba trabalho de outro nó NUMA, ele é forçado a processar dados distantes e invalida as caches do proprietário original.

    Runtimes assíncronos contemporâneos, como o escalonador padrão de Tokio em Rust ou o runtime da linguagem Go, utilizam filas de tarefas locais por thread de trabalho.

    Quando a fila local de um trabalhador esvazia, ele tenta furtar tarefas das filas de outros trabalhadores.

    Se essa operação de roubo cruza as fronteiras do nó NUMA, duas patologias graves acontecem simultaneamente:

    Em primeiro lugar, a tarefa roubada referencia estruturas e ponteiros que continuam residentes na DRAM do nó de origem. O trabalhador executa o processamento sob constante penalidade de acesso remoto.

    Em segundo lugar, a manipulação de ponteiros atômicos na fila de trabalho remota dispara transmissões de invalidação de linha de cache (false sharing em barramento inter-socket).

    Para cargas com requisitos de cauda p99 rígidos, a solução arquitetural é migrar do modelo de work-stealing global para o modelo Thread-Per-Core (compartilhado-zero).

    Frameworks como Seastar Project e Glommio em Rust criam uma instância isolada do runtime por núcleo físico.

    Nesse modelo, threads nunca migram e tarefas nunca são roubadas entre núcleos distintos.

    A comunicação entre núcleos diferentes ocorre exclusivamente por meio de filas circulares Single-Producer Single-Consumer (SPSC) lock-free, com memória pré-alocada e dimensionada.

    MODELO WORK-STEALING GLOBAL (CONTENÇÃO CROSS-NUMA):
    [ Núcleo 0 (Nó 0) ] ----> Rouba Tarefa ----> [ Fila do Núcleo 32 (Nó 1) ]
             |                                                 |
             +=========== Barramento UPI / Infinity Fabric =====+
                          (Penalidade de 200 ns por acesso)
    
    MODELO THREAD-PER-CORE (ISOLAMENTO COMPLETO):
    [ Núcleo 0 (Nó 0) ] <---> Fila SPSC Ponto a Ponto <---> [ Núcleo 32 (Nó 1) ]
      (DRAM Local 0)                                          (DRAM Local 1)
    

    Servidor de computação de missão crítica montado em rack industrial com dutos de ar grafite e cabo de rede QSFP-DD com puxador e indicador luminoso de status em primeiro plano

    Como os alocadores de memória modernos lidam com múltiplos domínios de nós

    Alocadores de alta performance evitam travas globais particionando o gerenciamento do heap em arenas dedicadas por CPU ou por nó NUMA. Soluções como jemalloc, mimalloc e TCMalloc eliminam a disputa de memória entre threads em nós diferentes.

    Em sistemas multiprocessados, o alocador malloc tradicional da biblioteca glibc tornava-se um gargalo devastador ao exigir sincronização global para cada alocação.

    O jemalloc, amplamente adotado em infraestruturas de missão crítica, resolveu esse impasse associando arenas independentes a cada CPU lógica.

    Quando uma thread solicita memória, o alocador roteia a requisição para a arena correspondente ao nó NUMA atual da thread.

    Pequenos objetos são atendidos a partir de caches locais de thread (thread caches ou tcache), sem qualquer instrução de trava atômica.

    Alocadores modernos como o mimalloc da Microsoft e o TCMalloc da Google avançaram essa técnica utilizando Restartable Sequences (rseq).

    O recurso rseq do kernel Linux permite que operações de manipulação de pilha local da CPU sejam executadas sem locks atômicos caros. Se o escalonador interrompe a thread no meio da sequência, o kernel reinicia a operação transparentemente.

    Alocador de MemóriaEstratégia NUMA e ConcorrênciaMecanismo de Cache LocalVantagem Principal em Produção
    jemallocArenas mapeadas por nó NUMA e afinidade de CPUtcache por thread com reciclagem em lotesFragmentação mínima em heaps de longa duração
    mimallocPáginas livres particionadas com tamanhos fixosCaches de bloco locais e alinhamento de 64 bytesDesempenho extremo em alocações de vida curta
    TCMallocGerenciamento central com caches por CPUThreadCache e buffers locais baseados em rseqBaixíssimo overhead em contagens massivas de threads
    Glibc mallocArenas dinâmicas limitadas a 8× contagem de núcleosPools genéricos sem garantia estrita de nóAlto risco de false sharing e contenção de travas

    Como o MaxVision Code estrutura o isolamento NUMA em pipelines de agentes

    No ecossistema do MaxVision Code, o processamento de código em tempo real e a execução de agentes de IA exigem segregação estrita de nós para eliminar jitter. A infraestrutura particiona nós de hardware para isolar operações de rede e tarefas de compilação intensiva.

    Em um servidor de 128 núcleos físicos com topologia dual-socket, a arquitetura divide a carga de trabalho em dois domínios isolados:

    Primeiro, o Nó NUMA 0 (núcleos 0 a 31) fica dedicado à ingestão de rede e mensageria. Conectado diretamente à interface 100 GbE via barramento PCIe local, opera com numa_alloc_onnode para manter buffers de pacotes na DRAM local.

    Segundo, o Nó NUMA 1 (núcleos 32 a 63) assume os motores de execução e compilação de agentes. Executa parsers Tree-Sitter, compilações com rustc e inferência de modelos locais, totalmente livre das interrupções de hardware da rede.

    A comunicação entre a camada de ingestão no Nó 0 e os agentes no Nó 1 ocorre por meio de ring buffers circulares lock-free.

    Os cursores de leitura e escrita recebem espaçamento de 128 bytes (#[repr(align(128))] em Rust) para cobrir linhas de cache adjacentes e evitar pré-carregamento especulativo cruzado.

    Com esse particionamento de topologia, o tempo médio de resposta do pipeline no MaxVision Code despencou de 1,8 ms para 142 microssegundos.

    O percentil de cauda p99.9 caiu de 34 ms para 420 microssegundos, eliminando completamente as pausas provocadas por saturação no barramento de interconexão.

    Checklist de engenharia para produção em arquiteturas NUMA

    A sustentação de cargas de alta vazão exige auditoria sistemática das configurações de hardware, kernel e código de aplicação. O checklist a seguir resume as intervenções obrigatórias para garantir simpatia mecânica em servidores de produção.

    1. Configuração de Kernel e Inicialização

    • Configure isolamento de núcleos sensíveis utilizando parâmetros isolcpus=domain,managed_irq e nohz_full no GRUB.
    • Desloque o tratamento de interrupções de hardware da rede e do armazenamento para núcleos utilitários via smp_affinity.
    • Monitore e avalie o impacto do balanceador automático do kernel (sysctl kernel.numa_balancing=0 para desativar ruído de migração quando o particionamento for estrito).

    2. Gestão de Memória e Alocação

    • Garanta que estruturas compartilhadas de grande porte sejam inicializadas em paralelo (parallel first-touch) por todas as threads de trabalho.
    • Utilize páginas gigantes transparentes (THP) ou alocações dedicadas em hugetlbfs (páginas de 2 MB ou 1 GB) para reduzir faltas na tabela TLB (Translation Lookaside Buffer).
    • Adote alocadores modernos configurados para respeitar topologia local (jemalloc com arenas particionadas ou mimalloc).

    3. Código e Arquitetura de Software

    • Prefira o modelo Thread-Per-Core compartilhado-zero sobre work-stealing indiscriminado em tarefas de latência crítica.
    • Aplique espaçamento explícito de linha de cache (64 ou 128 bytes) em estruturas de dados compartilhadas entre nós para eliminar false sharing.
    • Audite regularmente métricas de hardware com contadores PMU para verificar a localidade de dados em execução real.

    Fontes e referências canônicas

    1. Drepper, Ulrich. "What Every Programmer Should Know About Memory." Red Hat / LWN.net, 2007. URL: https://lwn.net/Articles/72228/. Análise seminal sobre hierarquia de caches, microarquitetura de DRAM e topologia NUMA.
    2. The Linux Kernel Development Community. "NUMA Memory Policy." Linux Kernel Documentation (admin-guide/mm/numa_memory_policy.rst). URL: https://www.kernel.org/doc/html/latest/admin-guide/mm/numa_memory_policy.html. Especificação oficial das políticas de alocação de memória e chamadas mbind.
    3. Intel Corporation. "Intel 64 and IA-32 Architectures Optimization Reference Manual: Volume 1." Intel Developer Zone, 2024–2026. Documentação técnica cobrindo topologia Ultra Path Interconnect (UPI), Sub-NUMA Clustering (SNC) e protocolos de coerência.
    4. Advanced Micro Devices (AMD). "AMD EPYC 9004 Series Processor Architecture: High Performance Computing and Enterprise Solutions." AMD Developer Central, 2023–2025. Especificações de Infinity Fabric, Core Complex Dies (CCDs) e arquitetura de controladores de memória descentralizados.
    5. Kleen, Andi. "An NUMA API for Linux." Novell/SUSE Whitepaper e documentação canônica da biblioteca libnuma. URL: https://man7.org/linux/man-pages/man3/numa.3.html.
    6. Kivity, Avi; Laor, Dor et al. "Seastar: High Performance Server-Side Application Framework." ScyllaDB Project. URL: https://seastar.io/. Documentação e código-fonte da arquitetura de referência Thread-Per-Core e eliminação de travas.
    7. Evans, Jason. "jemalloc: Scalable Concurrent Memory Allocation." Meta / jemalloc Project. URL: https://jemalloc.net/. Arquitetura de particionamento de arenas por nós de memória e mitigação de contenção entre threads.
    8. Microsoft Corporation. "mimalloc: A compact general purpose allocator with excellent performance." Microsoft Research. URL: https://github.com/microsoft/mimalloc. Implementação de blocos livres, caches de alinhamento e integração com sequências reiniciáveis (rseq).
    TAGS
    • Engenharia de Software
    • Microarquitetura
    • Sistemas Distribuídos
    • Linux
    • Performance
    • Rust
    • MaxVision Code
    Mascote da MaxVision para contato rápido no WhatsAppFale agora pelo WhatsApp