O padrão C ++ exige desempenho ruim para o iostreams ou estou lidando apenas com uma implementação deficiente?

Toda vez que menciono o desempenho lento da biblioteca padrão do C ++, eu me deparo com uma onda de descrença. Ainda tenho resultados de profiler mostrando grandes quantidades de tempo gasto em código de biblioteca iostream (otimizações de compilador completo) e comutação de iostreams para APIs de E / S específicas de sistema operacional e gerenciamento de buffer personalizado dá uma melhoria de ordem de magnitude.

Que trabalho extra a biblioteca padrão C ++ está fazendo, é requerida pelo padrão e é útil na prática? Ou alguns compiladores fornecem implementações de iostreams que são competitivas com o gerenciamento de buffer manual?

Referências

Para resolver os problemas, escrevi alguns programas curtos para exercitar o buffer interno do iostreams:

  • colocando dados binários em um ostringstream http://ideone.com/2PPYw
  • colocando dados binários em um buffer char[] http://ideone.com/Ni5ct
  • colocando dados binários em um vector usando back_inserter http://ideone.com/Mj2Fi
  • NOVO : vector iterador simples vector http://ideone.com/9iitv
  • NOVO : colocando dados binários diretamente no stringbuf http://ideone.com/qc9QA
  • NOVO : vector simples iterador mais verificações de limites http://ideone.com/YyrKy

Observe que as versões ostringstream e stringbuf executam menos iterações porque são muito mais lentas.

No ideone, o stream de ostring é cerca de 3 vezes mais lento que std:copy + back_inserter + std::vector e cerca de 15 vezes mais lento que o memcpy em um buffer bruto. Isso é consistente com o perfil antes e depois de trocar meu aplicativo real para o buffer personalizado.

Esses são todos os buffers na memory, portanto a lentidão do iostreams não pode ser atribuída a E / S de disco lento, muita descarga, synchronization com o stdio ou qualquer outra coisa que as pessoas usam para desculpar a lentidão observada da biblioteca padrão C ++ iostream.

Seria bom ver benchmarks em outros sistemas e comentários sobre coisas que implementações comuns fazem (como libc ++ do gcc, Visual C ++, Intel C ++) e quanto do overhead é exigido pelo padrão.

Justificativa para este teste

Várias pessoas apontaram corretamente que os iostreams são mais comumente usados ​​para saída formatada. No entanto, eles também são a única API moderna fornecida pelo padrão C ++ para access a arquivos binários. Mas a verdadeira razão para fazer testes de desempenho no buffer interno se aplica à típica E / S formatada: se o iostreams não puder manter o controlador de disco fornecido com dados brutos, como eles podem acompanhar quando são responsáveis ​​pela formatação também?

Tempo de Referência

Todos estes são por iteração do loop externo ( k ).

Em ideone (gcc-4.3.4, sistema operacional desconhecido e hardware):

  • ostringstream : 53 milissegundos
  • stringbuf : 27 ms
  • vector e back_inserter : 17,6 ms
  • vector com iterador comum: 10,6 ms
  • vector iterador e verificação de limites: 11.4 ms
  • char[] : 3,7 ms

No meu laptop (Visual C ++ 2010 x86, cl /Ox /EHsc , Windows 7 Ultimate de 64 bits, Intel Core i7, 8 GB de RAM):

  • ostringstream : 73,4 milissegundos, 71,6 ms
  • stringbuf : 21,7 ms, 21,3 ms
  • vector e back_inserter : 34,6 ms, 34,4 ms
  • vector com iterador comum: 1.10 ms, 1.04 ms
  • vector iterador e verificação de limites: 1,11 ms, 0,87 ms, 1,12 ms, 0,89 ms, 1,02 ms, 1,14 ms
  • char[] : 1,48 ms, 1,57 ms

Visual C ++ 2010 x86, com Otimização Guiada por Perfil cl /Ox /EHsc /GL /c , link /ltcg:pgi , executar, link /ltcg:pgo , measure:

  • ostringstream : 61,2 ms, 60,5 ms
  • vector com iterador comum: 1,04 ms, 1,03 ms

Mesmo laptop, mesmo sistema operacional, usando o cygwin gcc 4.3.4 g++ -O3 :

  • ostringstream : 62,7 ms, 60,5 ms
  • stringbuf : 44.4 ms, 44.5 ms
  • vector e back_inserter : 13,5 ms, 13,6 ms
  • vector com iterador comum: 4,1 ms, 3,9 ms
  • vector iterador e verificação de limites: 4,0 ms, 4,0 ms
  • char[] : 3,57 ms, 3,75 ms

Mesmo laptop, Visual C ++ 2008 SP1, cl /Ox /EHsc :

  • ostringstream : 88,7 ms, 87,6 ms
  • stringbuf : 23,3 ms, 23,4 ms
  • vector e back_inserter : 26,1 ms, 24,5 ms
  • vector com iterador comum: 3,13 ms, 2,48 ms
  • vector iterador e verificação de limites: 2,97 ms, 2,53 ms
  • char[] : 1,52 ms, 1,25 ms

Mesmo laptop, compilador de 64 bits do Visual C ++ 2010:

  • ostringstream : 48,6 ms, 45,0 ms
  • stringbuf : 16,2 ms, 16,0 ms
  • vector e back_inserter : 26,3 ms, 26,5 ms
  • vector com iterador comum: 0,87 ms, 0,89 ms
  • vector iterador e verificação de limites: 0,99 ms, 0,99 ms
  • char[] : 1,25 ms, 1,24 ms

EDIT: Ran todas as vezes para ver quão consistente os resultados foram. IMO bastante consistente.

NOTA: No meu laptop, desde que eu possa poupar mais tempo de CPU do que o ideone permite, eu configurei o número de iterações para 1000 para todos os methods. Isso significa que a realocação e a realocação vector , que ocorrem apenas na primeira passagem, devem ter pouco impacto nos resultados finais.

EDIT: Opa, encontrou um bug no vector -com-comum-iterador, o iterador não estava sendo avançado e, portanto, havia muitos hits de cache. Eu queria saber como vector estava superando char[] . Não faz muita diferença, porém, o vector ainda é mais rápido que o char[] sob o VC ++ 2010.

Conclusões

O buffer de streams de saída requer três etapas sempre que os dados são anexados:

  • Verifique se o bloco de input se ajusta ao espaço disponível no buffer.
  • Copie o bloco de input.
  • Atualize o ponteiro de fim de dados.

O trecho de código mais recente que eu postei, ” vector simples iterador mais verificação de limites” não apenas faz isso, ele também aloca espaço adicional e move os dados existentes quando o bloco de input não se encheckbox. Como Clifford apontou, armazenar em buffer em uma class de E / S de arquivo não teria que fazer isso, seria apenas liberar o buffer atual e reutilizá-lo. Portanto, este deve ser um limite superior no custo da saída do buffer. E é exatamente o que é necessário para criar um buffer de memory em funcionamento.

Então por que o stringbuf 2.5x é mais lento no ideone e pelo menos 10 vezes mais lento quando eu testo? Não está sendo usado polimorficamente neste micro-benchmark simples, então isso não explica.

Não respondendo tanto às especificidades da sua pergunta quanto ao título: o Relatório Técnico de 2006 sobre Desempenho em C ++ possui uma seção interessante sobre IOStreams (p.68). O mais relevante para sua pergunta está na Seção 6.1.2 (“Velocidade de Execução”):

Como certos aspectos do processamento de IOStreams são distribuídos em múltiplas facetas, parece que o Padrão exige uma implementação ineficiente. Mas este não é o caso – usando alguma forma de pré-processamento, muito do trabalho pode ser evitado. Com um linker ligeiramente mais inteligente do que o normalmente usado, é possível remover algumas dessas ineficiências. Isto é discutido em §6.2.3 e §6.2.5.

Como o relatório foi escrito em 2006, espera-se que muitas das recomendações tenham sido incorporadas aos compiladores atuais, mas talvez não seja esse o caso.

Como você mencionou, as facetas podem não aparecer em write() (mas eu não diria que cegamente). Então, o que caracteriza? Executar o GProf em seu código de ostringstream compilado com o GCC fornece a seguinte divisão:

  • 44,23% em std::basic_streambuf::xsputn(char const*, int)
  • 34,62% ​​em std::ostream::write(char const*, int)
  • 12,50% no main
  • 6,73% em std::ostream::sentry::sentry(std::ostream&)
  • 0.96% em std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 0,96% em std::basic_ostringstream::basic_ostringstream(std::_Ios_Openmode)
  • 0.00% em std::fpos::fpos(long long)

Assim, a maior parte do tempo é gasto em xsputn , que eventualmente chama std::copy() após muita verificação e atualização das posições e buffers do cursor (dê uma olhada em c++\bits\streambuf.tcc para os detalhes).

Minha opinião sobre isso é que você se concentrou na pior das hipóteses. Toda a verificação que é executada seria uma pequena fração do trabalho total feito se você estivesse lidando com pedaços razoavelmente grandes de dados. Mas seu código está transferindo dados em quatro bytes por vez e incorrendo em todos os custos extras de cada vez. Evidentemente, seria evitado fazê-lo em uma situação da vida real – considere-se quão insignificante seria a penalidade se a write fosse feita em uma matriz de 1m em vez de 1m em um int. E, em uma situação real, podemos realmente apreciar os resources importantes do IOStreams, ou seja, seu design seguro para a memory e para o tipo seguro. Esses benefícios têm um preço, e você escreveu um teste que faz com que esses custos dominem o tempo de execução.

Estou um pouco desapontado com os usuários do Visual Studio por aí, que preferiam me dar um presente:

  • Na implementação do ostream no Visual Studio, o object de sentry (que é exigido pelo padrão) entra em uma seção crítica que protege o streambuf (que não é obrigatório). Isso não parece ser opcional, então você paga o custo da synchronization de threads mesmo para um stream local usado por um único thread, que não precisa de synchronization.

Isso prejudica o código que usa o ostringstream para formatar mensagens muito severamente. Usar o stringbuf evita diretamente o uso de sentry , mas os operadores de inserção formatados não podem trabalhar diretamente no streambuf s. Para o Visual C ++ 2010, a seção crítica está diminuindo ostringstream::write por um fator de três vs a subjacente chamada stringbuf::sputn .

Analisando os dados do profiler da beldaz no newlib , parece claro que a sentry do gcc não faz nada maluco como este. ostringstream::write em gcc leva apenas 50% mais tempo que stringbuf::sputn , mas o próprio stringbuf é muito mais lento que em VC ++. E ambos ainda se comparam muito desfavoravelmente ao uso de um vector para buffer de E / S, embora não com a mesma margem que sob o VC ++.

O problema que você vê é todo o overhead em torno de cada chamada para write (). Cada nível de abstração que você adiciona (char [] -> vector -> string -> ostringstream) adiciona mais algumas chamadas de function / retornos e outras informações de limpeza que – se você chamá-lo um milhão de vezes – summ.

Eu modifiquei dois dos exemplos em ideone para escrever dez inteiros de cada vez. O tempo de ostringstream passou de 53 para 6 ms (quase 10 x de melhoria) enquanto o loop de caracteres melhorou (3.7 para 1.5) – útil, mas apenas por um fator de dois.

Se você está preocupado com o desempenho, então você precisa escolher a ferramenta certa para o trabalho. O ostringstream é útil e flexível, mas há uma penalidade por usá-lo do jeito que você está tentando. char [] é um trabalho mais difícil, mas os ganhos de desempenho podem ser ótimos (lembre-se de que o gcc provavelmente includeá os memcpys para você também).

Resumindo, o ostringstream não está quebrado, mas quanto mais perto você chegar do metal, mais rápido será o seu código. Assembler ainda tem vantagens para algumas pessoas.

Para obter melhor desempenho, você precisa entender como os contêineres que você está usando funcionam. No seu exemplo de matriz char [], a matriz do tamanho requerido é alocada antecipadamente. Em seu exemplo de vetor e ostringstream, você está forçando os objects a alocar e realocar repetidamente e, possivelmente, copiar dados muitas vezes à medida que o object cresce.

Com std :: vector isso é facilmente resolvido inicializando o tamanho do vetor para o tamanho final, como você fez com o array char; em vez disso, você prejudica injustamente o desempenho redimensionando para zero! Isso dificilmente é uma comparação justa.

Com relação a ostringstream, pré-alocar o espaço não é possível, sugiro que seja um uso inapropriado. A class tem uma utilidade muito maior do que uma matriz char simples, mas se você não precisar desse utilitário, não o use, porque você pagará a sobrecarga em qualquer caso. Em vez disso, deve ser usado para o que é bom – formatar dados em uma string. C ++ fornece uma ampla gama de contêineres e um ostringstram é um dos menos apropriados para essa finalidade.

No caso do vetor e ostringstream você obtém proteção contra saturação de buffer, você não consegue isso com uma matriz char e essa proteção não vem de graça.