Queda de desempenho de pipes no Linux
(qsantos.fr)- Ao gravar dados em um pipe do Linux com
write, o desempenho ficou muito abaixo de uma simples gravação em memória; no ambiente de teste, ficou em torno de 17GB/s para escrita em pipe contra 167GB/s para escrita em buffer de memória - O gargalo não é explicado por uma única cópia de dados; dentro de
pipe_write, somam-se os custos de alocação de páginas, travas e rotinas de cópia no kernel vmspliceconecta o buffer do espaço do usuário ao pipe sem copiá-lo para o kernel, contornando caminhos caros como__alloc_pages,_raw_spin_lock_irqecopy_user_enhanced_fast_string- No caso de throughput do Fizz Buzz, a solução com
vmsplicechegou a 60.8GiB/s em um único núcleo e 208.3GiB/s em múltiplos núcleos; em outro experimento, um exemplo comvmspliceregistrou 210GB/s - Por causa de correções posteriores, a interpretação sobre a penalidade por não usar SIMD não é confiável, e a comunicação entre processos não termina apenas no cache L1, então não é realista esperar 167GB/s como throughput efetivo de pipe
Ponto de partida: a grande diferença criada por vmsplice
- Alguns programas usam a chamada de sistema
vmsplicepara mover dados mais rapidamente por pipes - Na competição de throughput de Fizz Buzz no Code Golf StackExchange, as soluções se dividiram claramente em dois grupos
- As soluções que não usam
vmspliceficam na casa de alguns GiB por segundo, e a solução de neil chega a 8.4GiB/s - As soluções que usam
vmsplicechegam a 15.5GiB/s com tkluck, 60.8GiB/s com ais523 e 208.3GiB/s em múltiplos núcleos com david
- As soluções que não usam
- Apenas reduzir a cópia entre espaço do usuário e espaço do kernel não parece suficiente para explicar uma diferença de cerca de 7 vezes em um único núcleo
- Em experimentos próprios, a solução de ais523 registrou 96.4GiB/s, e a de david alcançou 277GB/s usando 7 núcleos, cerca de 40GB/s por núcleo
Linha de base: escrita de memória em espaço do usuário
- Um programa em Rust que copia repetidamente um buffer de 32KiB em memória de espaço do usuário, sem chamadas de sistema, registrou 167GB/s no ambiente de teste
- Esse valor foi considerado compatível com a velocidade de escrita no cache L1 da CPU usada
- O sistema de teste usava Ryzen 9 7950X3D, DDR5 6000T/s, Debian 12 e Linux 6.1.0-18-amd64
- As opções de mitigação da CPU estavam desativadas com
mitigations=off
- No profiling com
ftrace, a maior parte do tempo apareceu em__memset_avx512_unaligned_erms - Mas correções posteriores limitaram essa interpretação
- A instrução no ponto de interrupção era
rep stos, que não é uma instrução AVX-512 - Mesmo restringindo a execução a AVX2 e SSE2, o throughput permaneceu em 167GB/s porque em todos os casos foi usado
rep stos
- A instrução no ponto de interrupção era
O custo real de escrever em um pipe com write
- Ao gravar um buffer do mesmo tamanho em um pipe com
stdout.write()e consumi-lo compv >/dev/null, o throughput cai para 17GB/s - O profiling mostrou que a maior parte do tempo é gasta dentro da chamada de sistema
write, com 95% especificamente dentro depipe_write - Dentro de
pipe_write, preparação de páginas, travas e cópia se somam como custo__alloc_pages: 36% do tempo total, preparando novas páginas de memória para o pipe__mutex_lock.constprop.0: 25% do tempo total, correspondente ao custo de travas para escrita no pipe_raw_spin_lock_irq: 5% do tempo total, também associado a travas relacionadas à escrita no pipecopy_user_enhanced_fast_string: cerca de 20% do tempo total, copiando dados do espaço do usuário para o kernel
- Como
pvconsome os dados movendo páginas para/dev/nullcomsplice, fica difícil reutilizar em loop o mesmo pequeno conjunto de páginas
A rotina de cópia do kernel e a interpretação corrigida
- Ao desassemblar
copy_user_enhanced_fast_string, vê-se que cópias de buffers grandes são tratadas com a instruçãoREP MOV - Essa função não é escrita em C, mas em assembly do kernel Linux, então não se trata de um caminho acidentalmente sem otimização do compilador
- No experimento original, chamar
rep movsbdiretamente no espaço do usuário produziu 80GB/s, o que levou à interpretação de que a rotina de cópia do kernel seria cerca de 2 vezes mais lenta - Depois, uma correção mostrou que as condições do experimento eram diferentes
- Dois buffers de 32KiB saturavam o cache de dados L1
- Com buffers de 16KiB, o desempenho subiu para 153GB/s
- Portanto, a interpretação original de que a ausência de instruções vetoriais na cópia do kernel impunha uma grande penalidade não é confiável
- Ainda assim, permanece o ponto de que o overhead de gerenciamento de memória é alto na escrita em pipes
Os caminhos do kernel que vmsplice evita
vmsplicereduz o custo de uso de pipes ao passar o buffer inteiro do espaço do usuário para o pipe sem copiá-lo para o kernel- O exemplo
./writeincluído empipes-speed-test, de Francesco, é usado como exemplo mínimo que escreve'X'infinitamente - Esse exemplo registrou 210GB/s, mas, como passa repetidamente o mesmo buffer para
vmsplice, ele difere de uma tarefa comum de geração de dados- Se o fluxo não for um fluxo constante de bytes, novos dados precisam ser colocados no buffer
- Nesse caso, o limite simples de escrita em memória de 167GB/s volta a importar
- Mesmo no caminho de
vmsplice,__mutex_lock.constprop.0ainda consome 37% do tempo - Porém,
__alloc_pages,_raw_spin_lock_irqecopy_user_enhanced_fast_string, vistos no caminho comwrite, não aparecem - Em vez disso,
add_to_pipe,import_ioveceiov_iter_get_pages2surgem como caminhos principais, mostrando quevmsplicecontorna as partes mais caras dewrite
Conclusões restantes e pontos de atenção
- Nos experimentos, o caminho de escrita em pipes do Linux com
writefoi cerca de 10 vezes mais lento do que uma simples escrita em memória - A conclusão original dizia que o custo de travas e de salvar/restaurar contexto SIMD era alto na escrita em pipes, e que
spliceevmsplicecontornavam isso - Após as correções, essa conclusão precisa ser vista de forma mais limitada
- O overhead de gerenciamento de memória no kernel continua sendo um fator importante para a perda de desempenho em pipes
- A interpretação de que não usar instruções vetoriais gera uma penalidade tão grande quanto se pensava é imprecisa
- Como a comunicação entre processos não pode ocorrer apenas dentro do cache L1, é difícil atingir 167GB/s em um throughput real que inclua também a leitura do pipe
- Alguns erros importantes foram corrigidos, e a confiabilidade dos resultados pode ser limitada; os números devem ser interpretados mais como indicação de tendência do que como valores absolutos
- Para aumentar o throughput de pipes, não basta apenas reduzir o número de chamadas de sistema; também é preciso observar os caminhos internos do kernel percorridos por
writee a forma como os buffers são tratados
1 comentários
Opiniões no Hacker News
Há um projeto paralelo tentando tratar desse problema: https://lwn.net/Articles/976836/
A ideia é criar uma chamada de sistema que obtenha um ring buffer para todos os descritores de arquivo compatíveis. Isso incluiria pipes; se as duas pontas aceitarem o uso de ring buffer, elas poderiam mapear o mesmo ring buffer para fazer E/S sem cópia e, em alguns casos, talvez nem fazer chamadas ao kernel. Estão procurando pessoas para colaborar
Quando o ring buffer fica cheio ou vazio, é possível fazer sleep/poll; fora isso, ele funciona sem locks e sem overhead de chamadas de sistema
ringbuffer_wait()também poderia sinalizar tornando-se pronto para leitura empoll()Mesmo um hiccup momentâneo basta para bagunçar completamente um pipe se não houver buffering suficiente, e o tamanho de buffer necessário varia de sistema para sistema
O motivo de
JMPnão ser simplesmenteRETé a opção CONFIG_RETHUNK. O que aparece na desmontagem doobjdumpé o resultado de substituirRETporJMP __x86_return_thunkhttps://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/lib/ret...
As instruções NOP no início e no fim da função não são para ftrace; elas vêm das macros ASM_CLAC/ASM_STAC. Essas macros deixam espaço que, em tempo de execução, é preenchido com as instruções CLAC/STAC quando X86_FEATURE_SMAP é detectado. Ambas as instruções têm 3 bytes, o mesmo número de NOPs
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/kernel/...
Chamar pipes do Linux de “lentos” é como chamar um Toyota Corolla de “lento”. Eles são rápidos o bastante, a menos que você esteja em um caso de uso extremo
Você está correndo de carro? E é uma modalidade em que velocidade importa mais que técnica? Então compre um carro mais rápido. Caso contrário, continue com o Corolla
Por isso, mesmo ganhos de eficiência minúsculos tornam economicamente sensato gastar bastante tempo otimizando
Uso pipes para várias finalidades há mais de 10 anos e nunca fui limitado pela velocidade do pipe; na maioria das vezes, o gargalo provavelmente era alguma ferramenta como tar, gzip, find, grep ou nc. Claro que essas ferramentas também são bem rápidas para o que fazem
O FFMpeg não pode incluir o SDK Non-Free no código-fonte, e salvar RGBA puro em arquivo é absurdamente impraticável. Por isso o pipe é a única opção, e há uma justificativa legítima para precisar de pipes de alta vazão
https://www.toyota.com/grcorolla/
Esses carros têm engenharia e desempenho impressionantes, e também são uma espécie de hack para contornar as regras que dificultaram trazer o GR Yaris originalmente pretendido para o mercado dos EUA. Acho que há contexto suficiente de engenharia/desempenho/hacking/mercado para o pessoal do HN levar na esportiva. Além disso, o presidente da empresa ainda dirige e controla pessoalmente o carro
Embora não seja o ponto central do texto, em CPUs modernas,
rep movsbé tão rápido quanto a versão vetorizada mais rápida. Isso porque a CPU sabe acelerá-loO nome da função do kernel
copy_user_enhanced_fast_stringtambém sugere isso. Os recursos de CPU relacionados são ERMS (Enhanced Repeat Move String, que tornarep movsbrápido acima de um certo tamanho) e FSRM (Fast Short Repeat Move String, que também acelera cópias curtas)rep movsbé rápido até certo limiar, mas depois disso armazenamentos comuns ou armazenamentos não temporais (non-temporal store) são mais rápidosTodos os limiares estão descritos em https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch...
E esses valores nem são fixos; Noah Goldstein ainda os atualiza todos os anos
memcpycurtos, ainda estou esperando querep movsberep stosbfiquem rápidos o bastante para podermos remover versões simples em loop Cmemcpy()de tamanho variável, assim como fazem commemcpyde tamanho fixoHá um ponto sobre AVX512 que não vi no texto: além do overhead de
xsave/xrstor, AVX512 consome muita energia e provoca escalonamento da frequência da CPU. Para mais detalhes e para ver como isso pode ficar sutil, veja [1], [2][1] https://www.intel.com/content/dam/www/central-libraries/us/e...
[2] https://www.intel.com/content/www/us/en/developer/articles/t...
Quase todas as formas de comunicação entre processos são “lentas”. É uma escolha de pagar um custo de desempenho em troca de segurança
Em especial, não há muitos motivos para a comunicação entre processos mais rápida ser mais lenta do que uma chamada de função longa
Está recebendo de novo o hug of death do Hacker News. Graças ao cache de páginas do WordPress, está melhor do que da última vez, mas ainda assim a página pode levar alguns segundos para carregar, então peço compreensão
Nunca entendi muito bem por que
spliceprecisava ser tão lento. O texto aponta alocação de buffers e uso de instruções escalares como motivos para ser mais lento quevmsplice, mas não sei por que isso é necessárioPor que
splicesimplesmente não poderia ser reimplementado comovmsplice? Deve haver um bom motivo, mas acho que deixei passarvmsplicenão funciona com todos os tipos de descritores de arquivoSeria interessante ver também uma versão usando io_uring. Daria para compartilhar previamente buffers com o kernel e evitar algumas cópias, e talvez também evitar o overhead de chamadas de sistema. Ainda assim, neste caso, este último parece quase desprezível
É uma afirmação bastante ousada para um blog que leva uns 20 segundos para carregar
O artigo em si parece excelente, e há muito a aprender sobre o que acontece por baixo dos panos