Toda vez que menciono o desempenho lento dos iostreams da biblioteca padrão C ++, recebo uma onda de descrença. No entanto, tenho resultados do criador de perfil que mostram grandes quantidades de tempo gasto no código da biblioteca iostream (otimizações completas do compilador), e a mudança de iostreams para APIs de E / S específicas do SO e o gerenciamento de buffer personalizado oferecem uma melhoria de ordem de magnitude.
Que trabalho extra está fazendo a biblioteca padrão C ++, é exigida pelo padrão e é útil na prática? Ou alguns compiladores fornecem implementações de iostreams que são competitivas com o gerenciamento manual de buffer?
Benchmarks
Para agilizar, 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
char[]
buffer http://ideone.com/Ni5ct - colocando dados binários em um
vector<char>
usoback_inserter
http://ideone.com/Mj2Fi - NOVO :
vector<char>
iterador simples http://ideone.com/9iitv - NOVO : colocar dados binários diretamente em
stringbuf
http://ideone.com/qc9QA - NOVO : verificação
vector<char>
simples do iterador mais 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, ostringstream
é cerca de três vezes mais lento que std:copy
+ back_inserter
+ std::vector
e cerca de 15 vezes mais lento que memcpy
em um buffer bruto. Isso parece consistente com a criação de perfis antes e depois quando mudei meu aplicativo real para o buffer personalizado.
Esses são todos os buffers da memória, portanto, a lentidão dos iostreams não pode ser atribuída à E / S lenta do disco, descarga demais, sincronização com stdio ou qualquer outra coisa que as pessoas usam para desculpar a lentidão observada da biblioteca padrão C ++ iostream.
Seria bom ver referências em outros sistemas e comentários sobre as coisas que as implementações comuns fazem (como libc ++, Visual C ++, Intel C ++ da gcc) e quanto da sobrecarga é exigida 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 acesso a arquivos binários. Mas a verdadeira razão para a realização de testes de desempenho no buffer interno aplica-se às E / S formatadas típicas: se os iostreams não conseguem 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 k
loop externo ( ).
No ideone (gcc-4.3.4, SO e hardware desconhecidos):
ostringstream
: 53 milissegundosstringbuf
: 27 msvector<char>
eback_inserter
: 17,6 msvector<char>
com iterador comum: 10,6 msvector<char>
iterador e verificação de limites: 11,4 mschar[]
: 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 msstringbuf
: 21,7 ms, 21,3 msvector<char>
eback_inserter
: 34,6 ms, 34,4 msvector<char>
com iterador comum: 1,10 ms, 1,04 msvector<char>
verificação do iterador e dos limites: 1,11 ms, 0,87 ms, 1,12 ms, 0,89 ms, 1,02 ms, 1,14 mschar[]
: 1,48 ms, 1,57 ms
Visual C ++ 2010 x 86, com Guided-perfil de otimização cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, run, link /ltcg:pgo
, medida:
ostringstream
: 61,2 ms, 60,5 msvector<char>
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 msstringbuf
: 44,4 ms, 44,5 msvector<char>
eback_inserter
: 13,5 ms, 13,6 msvector<char>
com iterador comum: 4,1 ms, 3,9 msvector<char>
iterador e verificação de limites: 4,0 ms, 4,0 mschar[]
: 3,57 ms, 3,75 ms
Mesmo laptop, Visual C ++ 2008 SP1, cl /Ox /EHsc
:
ostringstream
: 88,7 ms, 87,6 msstringbuf
: 23,3 ms, 23,4 msvector<char>
eback_inserter
: 26,1 ms, 24,5 msvector<char>
com iterador comum: 3,13 ms, 2,48 msvector<char>
verificação de iterador e limites: 2,97 ms, 2,53 mschar[]
: 1,52 ms, 1,25 ms
Mesmo laptop, compilador de 64 bits do Visual C ++ 2010:
ostringstream
: 48,6 ms, 45,0 msstringbuf
: 16,2 ms, 16,0 msvector<char>
eback_inserter
: 26,3 ms, 26,5 msvector<char>
com iterador comum: 0,87 ms, 0,89 msvector<char>
verificação de iterador e limites: 0,99 ms, 0,99 mschar[]
: 1,25 ms, 1,24 ms
Edição: executou todas as duas vezes para ver o quão consistente os resultados foram. IMO bastante consistente.
NOTA: No meu laptop, como posso poupar mais tempo de CPU do que a ideona permite, defino o número de iterações para 1000 em todos os métodos. Isto significa que ostringstream
e vector
realocação, que acontece apenas na primeira passagem, deve ter pouco impacto sobre os resultados finais.
EDIT: Opa, encontrei um bug no vector
iterador -com-ordinário, o iterador não estava sendo avançado e, portanto, havia muitos acertos no cache. Fiquei me perguntando como vector<char>
estava superando char[]
. Porém, não fez muita diferença, vector<char>
ainda é mais rápido do que char[]
no VC ++ 2010.
Conclusões
O buffer dos fluxos de saída requer três etapas sempre que os dados são anexados:
- Verifique se o bloco de entrada se encaixa no espaço disponível no buffer.
- Copie o bloco recebido.
- Atualize o ponteiro de fim de dados.
O último trecho de código que eu publiquei, " vector<char>
iterador simples mais verificação de limites" não apenas faz isso, mas também aloca espaço adicional e move os dados existentes quando o bloco de entrada não se encaixa. Como Clifford apontou, o buffer em uma classe de E / S de arquivo não precisaria fazer isso, apenas liberaria o buffer atual e o reutilizaria. Portanto, esse deve ser um limite superior ao custo da produção de buffer. E é exatamente o que é necessário para criar um buffer de memória funcional.
Então, por que o stringbuf
ideone é 2,5x mais lento e pelo menos 10 vezes mais lento quando testo? Ele não está sendo usado polimorficamente neste micro-benchmark simples, o que não explica isso.
std::ostringstream
não for inteligente o suficiente para aumentar exponencialmente seu tamanho de buffer std::vector
, isso é (A) estúpido e (B) algo que as pessoas que pensam sobre o desempenho de E / S devem pensar. De qualquer forma, o buffer é reutilizado, não é realocado toda vez. E std::vector
também está usando um buffer que cresce dinamicamente. Estou tentando ser justo aqui.
ostringstream
e deseja um desempenho o mais rápido possível, considere ir diretamente para stringbuf
. As ostream
classes devem combinar a funcionalidade de formatação com reconhecimento de localidade com a opção flexível de buffer (arquivo, string, etc.) rdbuf()
e sua interface de função virtual. Se você não está fazendo nenhuma formatação, esse nível extra de indireção certamente parecerá proporcionalmente caro em comparação com outras abordagens.
ofstream
para fprintf
quando produzimos informações de registro envolvendo dobras. MSVC 2008 no WinXPsp3. iostreams é apenas um cão lento.