Já existem muitas respostas boas que cobrem muitos dos pontos mais importantes, então adicionarei algumas questões que não vi abordadas diretamente acima. Ou seja, essa resposta não deve ser considerada abrangente dos prós e contras, mas sim um adendo a outras respostas aqui.
mmap parece mágica
Tomando o caso em que o arquivo já está totalmente em cache 1 como linha de base de 2 , mmap
pode parecer bastante mágico :
mmap
requer apenas 1 chamada do sistema para (potencialmente) mapear o arquivo inteiro, após o qual não são necessárias mais chamadas do sistema.
mmap
não requer uma cópia dos dados do arquivo do kernel para o espaço do usuário.
mmap
permite acessar o arquivo "como memória", inclusive processando-o com quaisquer truques avançados que você possa fazer com relação à memória, como auto-vetorização do compilador, intrínseca do SIMD , pré-busca, rotinas otimizadas de análise na memória, OpenMP, etc.
No caso de o arquivo já estar no cache, parece impossível: basta acessar diretamente o cache da página do kernel como memória e ele não pode ficar mais rápido que isso.
Bem, pode.
mmap não é realmente mágico porque ...
O mmap ainda funciona por página
Um custo oculto primário de mmap
vs read(2)
(que é realmente o syscall comparável no nível do SO para blocos de leitura ) é o demmap
você precisará fazer "algum trabalho" para cada página de 4K no espaço do usuário, mesmo que possa estar oculta pelo mecanismo de falha de página.
Por exemplo, uma implementação típica que apenas mmap
s o arquivo inteiro precisará executar uma falha de modo que 100 GB / 4K = 25 milhões de falhas para ler um arquivo de 100 GB. Agora, essas serão falhas menores , mas 25 bilhões de páginas ainda não serão super rápidas. O custo de uma falha menor provavelmente está nos 100s dos nanos, na melhor das hipóteses.
O mmap depende muito do desempenho do TLB
Agora, você pode passar MAP_POPULATE
para o mmap
comando para configurar todas as tabelas de páginas antes de retornar, para que não haja falhas na página ao acessá-lo. Agora, isso tem o pequeno problema de que ele também lê o arquivo inteiro na RAM, que explodirá se você tentar mapear um arquivo de 100 GB - mas vamos ignorá-lo por enquanto 3 . O kernel precisa executar um trabalho por página para configurar essas tabelas de páginas (aparece como hora do kernel). Isso acaba sendo um grande custo na mmap
abordagem e é proporcional ao tamanho do arquivo (ou seja, não fica relativamente menos importante à medida que o tamanho do arquivo aumenta) 4 .
Por fim, mesmo no acesso ao espaço do usuário, esse mapeamento não é exatamente gratuito (comparado aos grandes buffers de memória que não são originários de um arquivo mmap
) - mesmo depois que as tabelas de páginas são configuradas, cada acesso a uma nova página vai, conceitualmente, incorrem em uma falha no TLB. Como mmap
incluir um arquivo significa usar o cache da página e suas páginas em 4K, você incorre novamente nesse custo 25 milhões de vezes para um arquivo de 100 GB.
Agora, o custo real dessas falhas de TLB depende muito de pelo menos os seguintes aspectos do seu hardware: (a) quantos TLB de 4K você possui e como o restante do cache de tradução funciona (b) quão bem a pré-busca de hardware lida com com o TLB - por exemplo, a pré-busca pode desencadear uma caminhada de página? (c) quão rápido e quão paralelo é o hardware de deslocamento da página. Nos modernos processadores Intel x86 de ponta, o hardware de deslocamento de página é geralmente muito forte: há pelo menos 2 caminhantes de páginas paralelas, um passeio de página pode ocorrer simultaneamente com a execução contínua e a pré-busca de hardware pode acionar um passeio de página. Portanto, o impacto TLB em um streaming carga de leitura de é bastante baixo - e essa carga geralmente terá desempenho semelhante, independentemente do tamanho da página. Outro hardware é geralmente muito pior, no entanto!
read () evita essas armadilhas
O read()
syscall, que é o que geralmente subjaz às chamadas do tipo "leitura de bloco" oferecidas, por exemplo, em C, C ++ e outras linguagens, tem uma desvantagem principal da qual todos estão cientes:
- Cada
read()
chamada de N bytes deve copiar N bytes do kernel para o espaço do usuário.
Por outro lado, evita a maioria dos custos acima - você não precisa mapear em 25 milhões de páginas 4K no espaço do usuário. Você pode geralmentemalloc
usar um único buffer pequeno no espaço do usuário e reutilizá-lo repetidamente para todos os seusread
chamadas. No lado do kernel, quase não há problemas com páginas 4K ou falhas de TLB porque toda a RAM geralmente é mapeada linearmente usando algumas páginas muito grandes (por exemplo, páginas de 1 GB no x86), portanto as páginas subjacentes no cache da página são cobertas muito eficientemente no espaço do kernel.
Então, basicamente, você tem a seguinte comparação para determinar qual é mais rápido para uma única leitura de um arquivo grande:
O trabalho extra por página é implícito na mmap
abordagem mais caro do que o trabalho por byte de copiar o conteúdo do arquivo do kernel para o espaço do usuário implicado pelo uso read()
?
Em muitos sistemas, eles são realmente aproximadamente equilibrados. Observe que cada um é dimensionado com atributos completamente diferentes do hardware e da pilha do SO.
Em particular, a mmap
abordagem se torna relativamente mais rápida quando:
- O sistema operacional possui manipulação rápida de falhas menores e otimizações de volume especialmente de falhas menores, como falhas ao redor.
- O sistema operacional possui uma boa
MAP_POPULATE
implementação que pode processar mapas grandes com eficiência nos casos em que, por exemplo, as páginas subjacentes são contíguas na memória física.
- O hardware tem um forte desempenho de tradução de páginas, como TLBs grandes, TLBs rápidos de segundo nível, caminhantes de páginas rápidos e paralelos, boa interação de pré-busca com tradução e assim por diante.
... enquanto a read()
abordagem se torna relativamente mais rápida quando:
- O
read()
syscall tem bom desempenho de cópia. Por exemplo, bom copy_to_user
desempenho no lado do kernel.
- O kernel possui uma maneira eficiente (relativa à terra do usuário) de mapear a memória, por exemplo, usando apenas algumas páginas grandes com suporte de hardware.
- O kernel possui syscalls rápidos e uma maneira de manter as entradas TLB do kernel entre os syscalls.
Os fatores de hardware acima variam muito entre plataformas diferentes, mesmo dentro da mesma família (por exemplo, dentro das gerações x86 e especialmente segmentos de mercado) e definitivamente entre arquiteturas (por exemplo, ARM x x86 x PPC).
Os fatores do SO também mudam, com várias melhorias nos dois lados, causando um grande salto na velocidade relativa de uma abordagem ou de outra. Uma lista recente inclui:
- Adição de falha, descrita acima, o que realmente ajuda o
mmap
caso sem MAP_POPULATE
.
- Adição de
copy_to_user
métodos de atalho arch/x86/lib/copy_user_64.S
, por exemplo, usando REP MOVQ
quando é rápido, o que realmente ajuda o read()
caso.
Atualização após Spectre e Meltdown
As atenuações para as vulnerabilidades Spectre e Meltdown aumentaram consideravelmente o custo de uma chamada do sistema. Nos sistemas que medi, o custo de uma chamada de sistema "não faça nada" (que é uma estimativa da sobrecarga pura da chamada de sistema, além de qualquer trabalho real realizado pela chamada) passou de cerca de 100 ns em uma configuração típica. sistema Linux moderno para cerca de 700 ns. Além disso, dependendo do seu sistema, a correção de isolamento da tabela de páginas especificamente para o Meltdown pode ter efeitos adicionais a jusante, além do custo direto das chamadas do sistema devido à necessidade de recarregar as entradas TLB.
Tudo isso é uma desvantagem relativa para read()
métodos baseados em comparação mmap
com métodos baseados, pois os read()
métodos devem fazer uma chamada de sistema para cada valor de "tamanho do buffer" dos dados. Você não pode aumentar arbitrariamente o tamanho do buffer para amortizar esse custo, pois o uso de buffers grandes geralmente apresenta desempenho pior, pois você excede o tamanho L1 e, portanto, sofre constantemente falhas de cache.
Por outro lado, com mmap
, você pode mapear em uma grande região da memória MAP_POPULATE
e acessá-la com eficiência, ao custo de apenas uma única chamada do sistema.
1 Isso inclui mais ou menos o caso em que o arquivo não foi totalmente armazenado em cache para começar, mas onde a leitura antecipada do sistema operacional é boa o suficiente para fazer com que pareça assim (ou seja, a página geralmente é armazenada em cache na hora em que você eu quero isso). Esta é uma questão sutil, porque embora o caminho read-ahead obras muitas vezes é bastante diferente entre mmap
e read
chamadas, e pode ser ainda mais modificada chamadas "aconselhar", conforme descrito no 2 .
2 ... porque se o arquivo não for armazenado em cache, seu comportamento será completamente dominado pelas preocupações de E / S, incluindo a simpatia do seu padrão de acesso ao hardware subjacente - e todo o seu esforço deve ser para garantir que esse acesso seja tão compreensivo quanto possível, por exemplo, através do uso de madvise
ou fadvise
chamadas (e qualquer alteração no nível do aplicativo que você possa fazer para melhorar os padrões de acesso).
3 Você pode contornar isso, por exemplo, sequencialmente mmap
em janelas de tamanho menor, digamos 100 MB.
4 De fato, verifica-se que a MAP_POPULATE
abordagem é (pelo menos uma combinação de hardware / sistema operacional) apenas um pouco mais rápida do que não usá-lo, provavelmente porque o kernel está usando uma falha ao redor - de modo que o número real de falhas menores é reduzido por um fator de 16 ou então.
mmap()
é 2-6 vezes mais rápido que o uso de syscalls, por exemploread()
.