- 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
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
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
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
Parece que o autor original interpretou errado a saída do comando
timeO tempo
systemé o tempo de CPU que o kernel usou em nome do processoSe, no exemplo, temos
real0.395s,user0.196s esys0.117s, então a CPU trabalhou por um total de apenas 313ms, e os 82ms restantes ficaram ociososOu 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
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
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 demalloc(), seria possível até especificar a memória do programa por nome de arquivo e deixar a persistência a cargo do SOMuito do design de software ainda está preso às restrições da era dos discos rígidos
fsync()continua sendo lentoPara persistência real, é preciso outra abordagem, independentemente de ser disco giratório ou não
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/writeEm 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
São ineficientes como estações remotas de trabalho para desenvolvedores
Telegram e FB Messenger são rápidos, mas Teams e Skype não
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
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
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