2 pontos por GN⁺ 2026-01-08 | 1 comentários | Compartilhar no WhatsApp
  • Verifica por meio de experimentos o desequilíbrio entre desempenho de I/O e velocidade de processamento da CPU discutido recentemente, mostrando que, na prática, a CPU ainda é a principal limitação
  • A velocidade de leitura sequencial chega a 1,6GB/s com cache frio e 12,8GB/s com cache quente, mas a contagem de frequência de palavras em thread única fica em torno de 278MB/s
  • A estrutura de branches do código impede a vectorization, e mesmo com uma otimização simples de conversão para minúsculas o desempenho sobe apenas para cerca de 330MB/s
  • Até o comando wc -w alcança apenas 245MB/s, confirmando que o gargalo está no processamento da CPU e no tratamento de branches, não no disco
  • Com vectorization manual baseada em AVX2, o desempenho foi elevado até 1,45GB/s, mas ainda assim isso representa apenas cerca de 11% da velocidade de leitura sequencial, comprovando que o gargalo é a CPU, não o I/O

Comparação entre velocidade de I/O e desempenho da CPU

  • Seguindo a afirmação de Ben Hoyt, foi testado se o recente aumento da velocidade de leitura sequencial já ultrapassou a estagnação da velocidade da CPU
    • Medindo da mesma forma, foram registrados 1,6GB/s com cache frio e 12,8GB/s com cache quente
  • No entanto, ao executar a contagem de frequência de palavras em uma única thread, o resultado foi de apenas 278MB/s
    • Isso corresponde a cerca de 1/5 da velocidade de leitura do disco, mesmo com o cache aquecido

Experimento de contagem de frequência de palavras em C

  • Com GCC 12, optimized.c foi compilado com as opções -O3 -march=native e executado com um arquivo de entrada de 425MB
    • Resultado: 1,525 segundo, com velocidade de processamento de 278MB/s
  • Múltiplos branches e saídas antecipadas no código impediam a otimização de vectorization pelo compilador
    • Após mover a lógica de conversão para minúsculas para fora do loop, o desempenho melhorou para 330MB/s
    • Com Clang, a vectorization é realizada de forma mais eficiente

Comparação com a contagem simples de palavras (wc -w)

  • Foi executado o comando wc -w, que conta apenas o número de palavras em vez da frequência
    • Resultado: 245,2MB/s, mais lento do que o esperado
  • O wc processa vários caracteres de espaço em branco e caracteres definidos por locale, como ' ', '\n' e '\t'
    • Portanto, faz mais trabalho do que um código que separa apenas por espaço simples

Tentativa de vectorization com AVX2

  • Aproveitando recursos de CPUs modernas, foi implementada a vectorization com o conjunto de instruções AVX2
    • Uso de registradores de 256 bits e alinhamento de dados em 32 bits
    • Para comparar caracteres em branco, foi usada a instrução VPCMPEQB
  • A detecção de fronteiras de palavras foi feita com a máscara de bits (PMOVMSKB) e a instrução Find First Set (ffs)
    • A abordagem foi inspirada na implementação de strlen da Cosmopolitan libc

Resultados de desempenho e conclusão

  • O código com vectorization manual (wc-avx2) alcançou velocidade de processamento de 1,45GB/s
    • Foi verificado que o resultado é igual ao de wc -w (82.113.300 palavras)
  • Mesmo com cache frio, o tempo de computação em modo user ainda predomina
    • Isso confirma que o gargalo está no processamento da CPU, não no I/O de disco
  • No geral, a velocidade do disco é suficientemente alta, mas operações da CPU, como tratamento de branches e cálculo de hash, continuam sendo o fator limitante
  • O código e os resultados dos experimentos foram publicados no GitHub (haampie/wc-avx2)

1 comentários

 
GN⁺ 2026-01-08
Comentários do Hacker News
  • Acredito que o limite de desempenho das CPUs modernas é determinado pela quantidade de dados que um único núcleo consegue processar, ou seja, pela velocidade de memcpy()
    A maioria dos núcleos x86 fica em torno de 6GB/s, enquanto a série Apple M chega a cerca de 20GB/s
    Números como “200GB/s” divulgados em marketing são apenas a largura de banda agregada de todos os núcleos; um único núcleo ainda fica perto de 6GB/s
    Portanto, mesmo escrevendo um parser perfeito, não dá para ultrapassar esse limite
    Mas, usando um formato zero-copy, a CPU pode pular dados desnecessários e teoricamente “superar” os 6GB/s
    O formato Lite³, que estou desenvolvendo, usa esse princípio e apresenta desempenho até 120 vezes maior que o simdjson

    • Acho que os números apresentados para um único núcleo estão baixos demais
      Por exemplo, o Zen 1 mostra 25GB/s em um único núcleo (link de referência)
      Pelos resultados do microbenchmark que escrevi, o Zen 2 chega a 17GB/s sem AVX e até 35GB/s com AVX non-temporal
      No Apple M3 Max, foi medido até 125GB/s com NEON non-temporal
      Portanto, os números de 6GB/s para x86 e 20GB/s para Apple estão muito abaixo da realidade
    • Fico curioso sobre de onde vem esse limite — se é por causa da estrutura de barramento entre núcleo e cache, ou entre cache e controlador de memória
    • Fico curioso sobre por que a série Apple M tem uma largura de banda por núcleo 3 vezes maior que x86
    • Em chips modernos, é difícil saturar a largura de banda da memória só com a CPU; para isso, é preciso usar a iGPU
      Isso acontece porque a iGPU consegue acessar a memória unificada
      Então, para tarefas como cópia de grandes volumes de memória, parsing paralelo ou compressão/descompressão, é tecnicamente vantajoso usar a iGPU como blitter
      Ainda assim, o “pular” de que se fala em formatos zero-copy acontece em unidades de linha de cache
    • A Samsung anuncia SSDs NVMe com velocidade de leitura de 14GB/s, então é interessante pensar na relação disso com a ideia de um único núcleo de CPU a 6GB/s
  • Parece que o autor original interpretou errado a saída do comando time
    O tempo system é o tempo de CPU que o kernel usou em nome do processo
    Se, no exemplo, temos real 0.395s, user 0.196s e sys 0.117s, então a CPU trabalhou por um total de apenas 313ms, e os 82ms restantes ficaram ociosos
    Ou seja, ele de fato rodou mais rápido que o subsistema de disco, mas a diferença não é grande
    Além disso, o caminho de I/O está em estado CPU-bound — mesmo que o disco e o código fossem infinitamente rápidos, a execução do código de I/O no kernel ainda exigiria 117ms

  • Sou o autor do texto. Há uma continuação: I/O is no longer the bottleneck, part 2

    • Já participei de uma competição de contagem de frequência de palavras
      O texto de análise sobre as várias técnicas de otimização usadas pelos participantes é interessante
      Dependendo da complexidade do problema ou do número de classificações de caracteres em branco, a abordagem mudava
    • Se esse teste foi executado em um único núcleo, então o “limite de 6GB/s” citado acima foi refutado experimentalmente
  • O gargalo de desempenho nunca é sempre um único fator como “CPU ou I/O”, mas sim o recurso que satura primeiro na carga de trabalho real
    Pode ser CPU, largura de banda de memória, cache, disco, rede, locks, latência etc.
    Portanto, é preciso medir, provar com profiling e medir de novo depois das mudanças

  • O problema não é CPU nem I/O, mas o equilíbrio entre latência e throughput
    A maior parte do software é lenta porque ignora a latência
    Se os dados forem dispostos linearmente na memória, ou se forem aplicados processamento em lote e paralelismo, tudo fica muito mais rápido

  • Imagino uma arquitetura composta apenas por CPU ↔ cache ↔ armazenamento não volátil
    Se mmap() tivesse as mesmas características de desempenho de malloc(), seria possível até especificar a memória do programa por nome de arquivo e deixar a persistência a cargo do SO
    Muito do design de software ainda está preso às restrições da era dos discos rígidos

    • Mas fsync() continua sendo lento
      Para persistência real, é preciso outra abordagem, independentemente de ser disco giratório ou não
    • Também dá para implementar algo parecido no Linux
      Na prática, a maior parte das requisições de memória acontece via mmap()
      Só que, como o kernel tem dificuldade para prever o padrão de acesso, isso pode ser mais lento do que read/write
  • Em ambientes de nuvem, desempenho também pode virar mecanismo de ajuste de preço
    O hardware evoluiu de forma impressionante, mas alguns softwares (especialmente Windows ou apps de mensageria) parecem ter ficado ainda mais lentos

    • Na prática, instâncias de nuvem são 5 vezes mais lentas que um MacBook M1 e muito mais caras
      São ineficientes como estações remotas de trabalho para desenvolvedores
    • O motivo de a maioria dos apps com GUI ainda ser lenta continua sendo a espera por I/O
      Telegram e FB Messenger são rápidos, mas Teams e Skype não
    • Monitores CRT exibiam dados mais rapidamente
      Alguns LCDs têm latência de 500ms
  • Quando os SSDs NVMe surgiram, eu brincava dizendo que “agora é como ter 2TB de RAM”
    Mas hoje servidores com GPU realmente vêm com 2TB de RAM — uma engenharia impressionante

    • Já vi no passado uma configuração com 2TB de RAM DDR4 em um servidor Epyc usado por 5 mil dólares
      Me arrependo de não ter comprado naquela época
  • Pela minha experiência otimizando bancos de dados OLAP em ambientes com altíssima concorrência, o gargalo quase sempre era a velocidade da memória

  • Gargalo de I/O originalmente não era um conceito ligado a leitura sequencial, mas sim ao tempo de seek
    Entendo o ponto do texto, mas queria destacar isso

    • Graças a tecnologias modernas como CXL/PCIe, agora até RAM e controlador de memória podem ser vistos como um tipo de dispositivo de I/O
    • Em uma antiga aula de banco de dados, o desempenho de I/O era medido pelo tempo de seek do disco rígido
      Como a velocidade de leitura sequencial não podia ser melhorada por código, o essencial era a otimização de acessos não sequenciais