Pergunta

Estou trabalhando em um programa que processará arquivos com tamanho potencial de 100 GB ou mais.Os arquivos contêm conjuntos de registros de comprimento variável.Eu tenho uma primeira implementação instalada e funcionando e agora estou buscando melhorar o desempenho, principalmente para fazer E/S com mais eficiência, já que o arquivo de entrada é verificado muitas vezes.

Existe uma regra prática para usar mmap() versus leitura em blocos via C++ fstream biblioteca?O que eu gostaria de fazer é ler grandes blocos do disco em um buffer, processar registros completos do buffer e depois ler mais.

O mmap() o código pode ficar muito confuso, pois mmapOs blocos necessários precisam estar nos limites do tamanho da página (no meu entender) e os registros podem potencialmente ultrapassar os limites da página.Com fstreams, posso simplesmente procurar o início de um registro e começar a ler novamente, já que não estamos limitados à leitura de blocos que ficam nos limites do tamanho da página.

Como posso decidir entre essas duas opções sem primeiro escrever uma implementação completa?Quaisquer regras práticas (por exemplo, mmap() é 2x mais rápido) ou testes simples?

Foi útil?

Solução

Eu estava tentando encontrar a palavra final sobre desempenho de mmap/leitura no Linux e me deparei com uma postagem legal (link) na lista de discussão do kernel Linux.É de 2000, então houve muitas melhorias em IO e memória virtual no kernel desde então, mas explica bem o motivo mmap ou read pode ser mais rápido ou mais lento.

  • Uma chamada para mmap tem mais sobrecarga do que read (Assim como epoll tem mais sobrecarga do que poll, que tem mais sobrecarga do que read).Alterar mapeamentos de memória virtual é uma operação bastante cara em alguns processadores, pelas mesmas razões que a alternância entre diferentes processos é cara.
  • O sistema IO já pode usar o cache de disco, portanto, se você ler um arquivo, você atingirá ou perderá o cache, independentemente do método usado.

No entanto,

  • Os mapas de memória geralmente são mais rápidos para acesso aleatório, especialmente se seus padrões de acesso forem esparsos e imprevisíveis.
  • Os mapas de memória permitem que você manter usando páginas do cache até terminar.Isso significa que se você usar muito um arquivo por um longo período de tempo, fechá-lo e reabri-lo, as páginas ainda serão armazenadas em cache.Com read, seu arquivo pode ter sido liberado do cache há muito tempo.Isto não se aplica se você usar um arquivo e descartá-lo imediatamente.(Se você tentar mlock páginas apenas para mantê-las em cache, você está tentando enganar o cache do disco e esse tipo de tolice raramente ajuda no desempenho do sistema).
  • Ler um arquivo diretamente é muito simples e rápido.

A discussão sobre mmap/read me lembra duas outras discussões sobre desempenho:

  • Alguns programadores Java ficaram chocados ao descobrir que a E/S sem bloqueio geralmente é mais lenta do que a E/S com bloqueio, o que faz todo o sentido se você sabe que a E/S sem bloqueio requer a realização de mais syscalls.

  • Alguns outros programadores de rede ficaram chocados ao saber que epoll muitas vezes é mais lento do que poll, o que faz todo o sentido se você sabe que gerenciar epoll requer fazer mais syscalls.

Conclusão: Use mapas de memória se você acessar dados aleatoriamente, mantê-los por muito tempo ou se souber que pode compartilhá-los com outros processos (MAP_SHARED não é muito interessante se não houver compartilhamento real).Leia os arquivos normalmente se você acessar os dados sequencialmente ou descartá-los após a leitura.E se qualquer um dos métodos tornar seu programa menos complexo, faça que.Para muitos casos do mundo real, não há uma maneira segura de mostrar que alguém é mais rápido sem testar seu aplicativo real e NÃO um benchmark.

(Desculpe por fazer essa pergunta, mas eu estava procurando uma resposta e essa pergunta continuava aparecendo no topo dos resultados do Google.)

Outras dicas

O principal custo de desempenho será a E/S de disco."mmap()" é certamente mais rápido que istream, mas a diferença pode não ser perceptível porque a E/S do disco dominará seus tempos de execução.

Eu tentei o fragmento de código de Ben Collins (veja acima/abaixo) para testar sua afirmação de que "mmap() é caminho mais rápido" e não encontrou nenhuma diferença mensurável.Veja meus comentários sobre sua resposta.

eu certamente não recomendo mapear separadamente cada registro, a menos que seus "registros" sejam enormes - isso seria terrivelmente lento, exigindo 2 chamadas de sistema para cada registro e possivelmente perdendo a página do cache da memória do disco .....

No seu caso, acho que mmap(), istream e as chamadas open()/read() de baixo nível serão todas iguais.Eu recomendaria mmap() nestes casos:

  1. Há acesso aleatório (não sequencial) dentro do arquivo, E
  2. tudo cabe confortavelmente na memória OU há localidade de referência no arquivo para que certas páginas possam ser mapeadas e outras páginas mapeadas.Dessa forma, o sistema operacional usa a RAM disponível para obter o máximo benefício.
  3. OU se vários processos estiverem lendo/trabalhando no mesmo arquivo, então mmap() é fantástico porque todos os processos compartilham as mesmas páginas físicas.

(aliás - eu adoro mmap()/MapViewOfFile()).

mmap é caminho mais rápido.Você pode escrever um benchmark simples para provar isso a si mesmo:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

contra:

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

Claramente, estou omitindo detalhes (como determinar quando você chega ao final do arquivo, caso seu arquivo não seja múltiplo de page_size, por exemplo), mas realmente não deveria ser muito mais complicado do que isso.

Se puder, você pode tentar dividir seus dados em vários arquivos que podem ser editados com mmap() no todo, em vez de em parte (muito mais simples).

Alguns meses atrás, eu tinha uma implementação incompleta de uma classe de stream com janela deslizante mmap() para boost_iostreams, mas ninguém se importou e fiquei ocupado com outras coisas.Infelizmente, apaguei um arquivo de projetos antigos inacabados há algumas semanas, e essa foi uma das vítimas :-(

Atualizar:Devo também acrescentar a ressalva de que esse benchmark seria bem diferente no Windows porque a Microsoft implementou um cache de arquivo bacana que faz a maior parte do que você faria com o mmap em primeiro lugar.Ou seja, para arquivos acessados ​​com frequência, você poderia simplesmente fazer std::ifstream.read() e seria tão rápido quanto mmap, porque o cache do arquivo já teria feito um mapeamento de memória para você e é transparente.

Atualização final:Olha, pessoal:em várias combinações de plataformas diferentes de sistemas operacionais e bibliotecas padrão, discos e hierarquias de memória, não posso dizer com certeza se a chamada do sistema mmap, visto como uma caixa preta, sempre será substancialmente mais rápido do que read.Essa não era exatamente a minha intenção, mesmo que minhas palavras pudessem ser interpretadas dessa forma. Em última análise, meu argumento foi que a E/S mapeada em memória é geralmente mais rápida que a E/S baseada em bytes;isso ainda é verdade.Se você descobrir experimentalmente que não há diferença entre os dois, então a única explicação que me parece razoável é que sua plataforma implementa o mapeamento de memória nos bastidores de uma forma que seja vantajosa para o desempenho das chamadas para read.A única maneira de ter certeza absoluta de que você está usando E/S mapeada em memória de maneira portátil é usar mmap.Se você não se preocupa com portabilidade e pode confiar nas características específicas de suas plataformas alvo, então usar read pode ser adequado sem sacrificar mensuravelmente qualquer desempenho.

Edite para limpar a lista de respostas:@jbl:

A janela deslizante Mmap parece interessante.Você pode dizer um pouco mais sobre isso?

Claro - eu estava escrevendo uma biblioteca C++ para Git (uma libgit++, se preferir) e me deparei com um problema semelhante a este:Eu precisava ser capaz de abrir arquivos grandes (muito grandes) e não deixar o desempenho ser um problema total (como seria com std::fstream).

Boost::Iostreams já tem uma fonte mapped_file, mas o problema é que era mmapexecute ping em arquivos inteiros, o que limita você a 2^(wordsize).Em máquinas de 32 bits, 4 GB não é suficiente.Não é irracional esperar ter .pack arquivos no Git que se tornam muito maiores do que isso, então eu precisava ler o arquivo em pedaços sem recorrer à E/S de arquivo normal.Sob as cobertas de Boost::Iostreams, implementei um Source, que é mais ou menos outra visão da interação entre std::streambuf e std::istream.Você também pode tentar uma abordagem semelhante apenas herdando std::filebuf dentro de mapped_filebuf e da mesma forma, herdando std::fstream em a mapped_fstream.É a interação entre os dois que é difícil de acertar. Boost::Iostreams tem parte do trabalho feito para você e também fornece ganchos para filtros e cadeias, então pensei que seria mais útil implementá-lo dessa forma.

Já existem muitas respostas boas aqui que cobrem muitos dos pontos mais importantes, então acrescentarei apenas algumas questões que não vi abordadas diretamente acima.Ou seja, esta 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 armazenado em cache1 como linha de base2, mmap pode parecer muito com Magia:

  1. mmap requer apenas 1 chamada de sistema para (potencialmente) mapear o arquivo inteiro, após a qual não são necessárias mais chamadas de sistema.
  2. mmap não requer uma cópia dos dados do arquivo do kernel para o espaço do usuário.
  3. mmap permite que você acesse o arquivo "como memória", incluindo processá-lo com quaisquer truques avançados que você possa fazer na memória, como vetorização automática do compilador, SIMD intrínsecos, pré-busca, rotinas otimizadas de análise na memória, OpenMP, etc.

Caso o arquivo já esteja no cache, parece impossível vencer:você apenas acessa diretamente o cache da página do kernel como memória e não pode ficar mais rápido que isso.

Bem, pode.

mmap não é realmente mágico porque ...

mmap ainda faz trabalho por página

Um custo oculto primário de mmap contra read(2) (que é realmente o syscall de nível de sistema operacional comparável para blocos de leitura) é que com mmap você precisará fazer "algum trabalho" para cada página 4K no espaço do usuário, mesmo que ela possa estar oculta pelo mecanismo de falha de página.

Por exemplo, uma implementação típica que apenas mmapSerá necessário falha no arquivo inteiro, portanto 100 GB/4K = 25 milhões de falhas para ler um arquivo de 100 GB.Agora, estes serão pequenas falhas, mas 25 bilhões de falhas de página ainda não serão muito rápidas.O custo de uma falha menor está provavelmente na casa das centenas de nanos, na melhor das hipóteses.

mmap depende muito do desempenho do TLB

Agora você pode passar MAP_POPULATE para mmap para instruí-lo a configurar todas as tabelas de páginas antes de retornar, para que não haja falhas de página ao acessá-las.Agora, isso tem o pequeno problema de também ler o arquivo inteiro na RAM, o que vai explodir se você tentar mapear um arquivo de 100 GB - mas vamos ignorar isso por enquanto3.O kernel precisa fazer trabalho por página para configurar essas tabelas de páginas (aparece como horário do kernel).Isso acaba sendo um grande custo no mmap abordagem e é proporcional ao tamanho do arquivo (ou seja, não se torna relativamente menos importante à medida que o tamanho do arquivo aumenta)4.

Finalmente, mesmo no espaço do usuário, o acesso a esse mapeamento não é exatamente gratuito (em comparação com grandes buffers de memória não originados de um arquivo baseado em arquivo). mmap) - mesmo depois que as tabelas de páginas estiverem configuradas, cada acesso a uma nova página irá, conceitualmente, incorrer em uma falha de TLB.Desde mmapAo armazenar um arquivo significa usar o cache de páginas e suas páginas de 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) quantas entidades TLB 4K você possui e como funciona o restante do cache de tradução (b) quão bem a pré-busca de hardware lida com o TLB - por exemplo, a pré-busca pode acionar uma caminhada de página?(c) quão rápido e paralelo é o hardware de navegação de página.Nos modernos processadores Intel x86 de última geração, o hardware de navegação de página é em geral muito forte:há pelo menos dois caminhantes de página paralelos, 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 do TLB em um transmissão a carga de leitura é bastante baixa - e essa carga geralmente terá desempenho semelhante, independentemente do tamanho da página.Outros hardwares geralmente são muito piores!

read() evita essas armadilhas

O read() syscall, que geralmente é a base das chamadas do tipo "leitura de bloco" oferecidas, por exemplo, em C, C++ e outras linguagens, tem uma desvantagem principal que todos conhecem:

  • Todo read() a chamada de N bytes deve copiar N bytes do kernel para o espaço do usuário.

Por outro lado, evita a maior parte dos custos acima - você não precisa mapear 25 milhões de páginas 4K no espaço do usuário.Você geralmente pode malloc um único buffer pequeno no espaço do usuário e reutilizá-lo repetidamente para todos os seus read chamadas.No lado do kernel, quase não há problemas com páginas de 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 em x86), de modo que as páginas subjacentes no cache de páginas 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 no mmap abordagem mais cara do que o trabalho por byte de copiar o conteúdo do arquivo do kernel para o espaço do usuário implícito no uso read()?

Em muitos sistemas, eles são aproximadamente equilibrados.Observe que cada um é dimensionado com atributos completamente diferentes de hardware e pilha de sistema operacional.

Em particular, o mmap abordagem torna-se relativamente mais rápida quando:

  • O sistema operacional tem tratamento rápido de falhas menores e, especialmente, otimizações de volume de falhas menores, como solução de falhas.
  • O sistema operacional tem um bom 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 possui 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 o read() abordagem torna-se relativamente mais rápida quando:

  • O read() syscall tem bom desempenho de cópia.Ex.: bom copy_to_user desempenho no lado do kernel.
  • O kernel possui uma maneira eficiente (em relação à área 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 syscalls.

Os fatores de hardware acima variam descontroladamente em diferentes plataformas, mesmo dentro da mesma família (por exemplo, dentro de gerações x86 e especialmente segmentos de mercado) e definitivamente entre arquiteturas (por exemplo, ARM vs x86 vs PPC).

Os fatores do sistema operacional também continuam mudando, com diversas melhorias em ambos os lados causando um grande salto na velocidade relativa de uma abordagem ou de outra.Uma lista recente inclui:

  • Adição de falhas, descritas acima, o que realmente ajuda o mmap caso sem MAP_POPULATE.
  • Adição de caminho rápido copy_to_user métodos em 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 mitigações para as vulnerabilidades Spectre e Meltdown aumentaram consideravelmente o custo de uma chamada de sistema.Nos sistemas que medi, o custo de uma chamada de sistema "não fazer nada" (que é uma estimativa da sobrecarga pura da chamada de sistema, independentemente de qualquer trabalho real realizado pela chamada) passou de cerca de 100 ns em um típico sistema Linux moderno para cerca de 700 ns.Além disso, dependendo do seu sistema, o isolamento de tabela de páginas a correção específica para Meltdown pode ter efeitos posteriores adicionais além do custo direto da chamada do sistema devido à necessidade de recarregar entradas TLB.

Tudo isto é uma desvantagem relativa para read() métodos baseados em comparação com mmap métodos baseados, uma vez que read() os métodos devem fazer uma chamada de sistema para cada valor de "tamanho do buffer" de dados.Você não pode aumentar arbitrariamente o tamanho do buffer para amortizar esse custo, pois o uso de buffers grandes geralmente tem pior desempenho, 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 de memória com MAP_POPULATE e acessá-lo de forma eficiente, ao custo de apenas uma única chamada de sistema.


1 Isso mais ou menos também inclui 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 no momento que você deseja isto).Este é um problema sutil porque a forma como a leitura antecipada funciona é muitas vezes bastante diferente entre mmap e read chamadas e pode ser ajustado posteriormente por chamadas de "aconselhamento", conforme descrito em 2.

2 ...porque se o arquivo for não armazenado em cache, seu comportamento será completamente dominado por preocupações de IO, incluindo o quão simpático é o seu padrão de acesso ao hardware subjacente - e todo o seu esforço deve ser para garantir que esse acesso seja o mais simpático possível, por exemplo.através do uso de madvise ou fadvise chamadas (e quaisquer alterações no nível do aplicativo que você possa fazer para melhorar os padrões de acesso).

3 Você poderia contornar isso, por exemplo, sequencialmente mmapem janelas de tamanho menor, digamos 100 MB.

4 Na verdade, acontece que MAP_POPULATE abordagem é (pelo menos uma combinação de hardware/sistema operacional) apenas um pouco mais rápida do que não usá-la, provavelmente porque o kernel está usando solução de falha - portanto, o número real de falhas menores é reduzido por um fator de 16 ou mais.

Lamento que Ben Collins tenha perdido o código-fonte do mmap das janelas deslizantes.Seria bom ter isso no Boost.

Sim, mapear o arquivo é muito mais rápido.Você está essencialmente usando o subsistema de memória virtual do sistema operacional para associar memória ao disco e vice-versa.Pense nisso desta maneira:se os desenvolvedores do kernel do sistema operacional pudessem torná-lo mais rápido, eles o fariam.Porque isso torna tudo mais rápido:bancos de dados, tempos de inicialização, tempos de carregamento de programas, etc.

A abordagem da janela deslizante não é tão difícil, pois várias páginas contínuas podem ser mapeadas de uma só vez.Portanto, o tamanho do registro não importa, desde que o maior de qualquer registro caiba na memória.O importante é administrar a contabilidade.

Se um registro não começar em um limite getpagesize(), seu mapeamento deverá começar na página anterior.O comprimento da região mapeada se estende do primeiro byte do registro (arredondado para baixo, se necessário, para o múltiplo mais próximo de getpagesize()) até o último byte do registro (arredondado para o múltiplo mais próximo de getpagesize()).Quando terminar de processar um registro, você pode desmapá-lo() e passar para o próximo.

Tudo isso funciona bem no Windows também usando CreateFileMapping() e MapViewOfFile() (e GetSystemInfo() para obter SYSTEM_INFO.dwAllocationGranularity --- não SYSTEM_INFO.dwPageSize).

mmap deveria ser mais rápido, mas não sei quanto.Depende muito do seu código.Se você usar o mmap, é melhor mapear o arquivo inteiro de uma vez, isso tornará sua vida muito mais fácil.Um problema potencial é que se o seu arquivo for maior que 4 GB (ou na prática o limite for menor, geralmente 2 GB), você precisará de uma arquitetura de 64 bits.Portanto, se você estiver usando um ambiente 32, provavelmente não desejará usá-lo.

Dito isto, pode haver um caminho melhor para melhorar o desempenho.Você disse o arquivo de entrada é verificado várias vezes, se você puder lê-lo de uma só vez e terminar com ele, isso poderá ser muito mais rápido.

Concordo que a E/S do arquivo mmap será mais rápida, mas enquanto você compara o código, o exemplo do contador não deveria ser de alguma forma otimizado?

Ben Collins escreveu:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

Eu sugeriria também tentar:

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

E além disso, você também pode tentar deixar o tamanho do buffer do mesmo tamanho de uma página de memória virtual, caso 0x1000 não seja o tamanho de uma página de memória virtual em sua máquina...A E/S de arquivo IMHO mmap'd ainda vence, mas isso deve tornar as coisas mais próximas.

Talvez você deva pré-processar os arquivos, para que cada registro fique em um arquivo separado (ou pelo menos que cada arquivo tenha um tamanho compatível com mmap).

Você também poderia executar todas as etapas de processamento de cada registro antes de passar para o próximo?Talvez isso evitasse parte da sobrecarga de IO?

Na minha opinião, usar mmap() "apenas" alivia o desenvolvedor de ter que escrever seu próprio código de cache.Em um caso simples de "ler o arquivo exatamente uma vez", isso não será difícil (embora, como mlbrock aponta, você ainda salve a cópia da memória no espaço do processo), mas se você estiver indo e voltando no arquivo ou pulando bits e assim por diante, acredito que os desenvolvedores do kernel provavelmente fiz um trabalho melhor implementando o cache do que eu ...

Lembro-me de mapear um arquivo enorme contendo uma estrutura de árvore na memória anos atrás.Fiquei impressionado com a velocidade em comparação com a desserialização normal, que envolve muito trabalho na memória, como alocar nós de árvore e definir ponteiros.Na verdade, eu estava comparando uma única chamada com o MMAP (ou sua contraparte no Windows) com muitas (muitas) chamadas para as chamadas de novas e construtores do operador.Para esse tipo de tarefa, o mmap é imbatível comparado à desserialização.É claro que se deve procurar aumentar o ponteiro relocável para isso.

Parece um bom caso de uso para multithreading ...Eu acho que você poderia facilmente configurar um thread para ler dados enquanto os outros os processam.Essa pode ser uma forma de aumentar drasticamente o desempenho percebido.Apenas um pensamento.

Acho que a melhor coisa do mmap é o potencial para leitura assíncrona com:

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

O problema é que não consigo encontrar o MAP_FLAGS correto para dar uma dica de que essa memória deve ser sincronizada do arquivo o mais rápido possível.Espero que MAP_POPULATE dê a dica certa para mmap (ou seja,ele não tentará carregar todo o conteúdo antes de retornar da chamada, mas fará isso de forma assíncrona.com feed_data).Pelo menos dá melhores resultados com este sinalizador, mesmo que o manual afirme que não faz nada sem MAP_PRIVATE desde 2.6.23.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top