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.
fonte
std::ostringstream
não for inteligente o suficiente para aumentar exponencialmente seu tamanho de bufferstd::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. Estd::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 parastringbuf
. Asostream
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
parafprintf
quando produzimos informações de registro envolvendo dobras. MSVC 2008 no WinXPsp3. iostreams é apenas um cão lento.Respostas:
Não respondendo tanto às especificidades de 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"):
Desde que o relatório foi escrito em 2006, seria de esperar que muitas das recomendações fossem incorporadas aos compiladores atuais, mas talvez esse não seja o caso.
Como você mencionou, facetas podem não aparecer
write()
(mas eu não assumiria isso cegamente). Então, o que apresenta? A execução do GProf no seuostringstream
código compilado com o GCC fornece a seguinte discriminação:std::basic_streambuf<char>::xsputn(char const*, int)
std::ostream::write(char const*, int)
main
std::ostream::sentry::sentry(std::ostream&)
std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
std::fpos<int>::fpos(long long)
Assim, a maior parte do tempo é gasta
xsputn
, o que eventualmente ocorrestd::copy()
após muitas verificações e atualizações das posições e buffers do cursor (consultec++\bits\streambuf.tcc
os detalhes).Minha opinião é que você se concentrou na pior situação possível. Toda a verificação executada seria uma pequena fração do trabalho total realizado se você estivesse lidando com blocos de dados razoavelmente grandes. Mas seu código está alterando os dados em quatro bytes por vez e incorrendo em todos os custos extras a cada vez. Claramente, alguém evitaria fazê-lo em uma situação da vida real - considere o quão insignificante a penalidade teria sido se
write
fosse invocada em uma matriz de 1 milhão de polegadas em vez de 1 milhão de vezes em uma int. E em uma situação da vida real, realmente apreciaríamos os recursos importantes do IOStreams, a saber, seu design com segurança de memória e de tipo. 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.fonte
ostream::write()
.sizeof i
, mas todos os compiladores com os quais estou testando têm 4 bytesint
). E isso não parece tão irreal para mim, que tamanho de pedaços você acha que são passados em cada chamadaxsputn
no código típico comostream << "VAR: " << var.x << ", " << var.y << endl;
.xsputn
cinco vezes, pode muito bem estar dentro de um loop que grava um arquivo de 10 milhões de linhas. Passar dados para iostreams em grandes blocos é muito menos um cenário da vida real do que o meu código de referência. Por que devo gravar em um fluxo em buffer com o número mínimo de chamadas? Se eu tiver que fazer meu próprio buffer, qual é o sentido dos iostreams? E com os dados binários, eu tenho a opção de armazená-los em buffer, ao escrever milhões de números em um arquivo de texto, a opção em massa simplesmente não existe, preciso chamaroperator <<
cada um.Estou um pouco decepcionado com os usuários do Visual Studio por aí, que preferiram me dar um presente:
ostream
, osentry
objeto (exigido pelo padrão) entra em uma seção crítica que protege ostreambuf
(que não é obrigatório). Isso não parece ser opcional; portanto, você paga o custo da sincronização de encadeamentos, mesmo para um fluxo local usado por um único encadeamento, que não precisa ser sincronizado.Isso prejudica o código usado
ostringstream
para formatar as mensagens bastante severamente. O usostringbuf
direto evita o uso desentry
, mas os operadores de inserção formatados não podem trabalhar diretamente emstreambuf
s. Para o Visual C ++ 2010, a seção crítica está desacelerandoostringstream::write
em um fator de três versus astringbuf::sputn
chamada subjacente .Olhando para os dados do beldaz no newlib , parece claro que o gcc
sentry
não faz nada doido como esse.ostringstream::write
no gcc leva apenas cerca de 50% mais tempo do questringbuf::sputn
, mas emstringbuf
si é muito mais lento que no VC ++. E ambos ainda se comparam muito desfavoravelmente ao uso de umvector<char>
buffer de E / S, embora não pela mesma margem do VC ++.fonte
sentry
... "A sentinela de classe define uma classe que é responsável por executar operações de prefixo e sufixo seguros de exceção." e uma nota "O construtor e destruidor de sentinelas também podem executar operações adicionais dependentes da implementação". Também se pode supor, a partir do princípio C ++ de "você não paga pelo que não usa", que o comitê C ++ nunca aprovaria um requisito tão desnecessário. Mas sinta-se à vontade para fazer uma pergunta sobre a segurança do thread iostream.O problema que você vê está na sobrecarga de cada chamada para escrever (). Cada nível de abstração que você adiciona (char [] -> vetor -> string -> ostringstream) adiciona mais algumas chamadas / devoluções de função e outras bobagens de limpeza que - se você a chamar um milhão de vezes - são adicionadas.
Modifiquei dois dos exemplos no ideone para escrever dez ints de cada vez. O tempo ostringstream passou de 53 para 6 ms (quase 10 vezes), enquanto o loop de char melhorou (3,7 para 1,5) - útil, mas apenas por um fator de dois.
Se você está preocupado com o desempenho, precisa escolher a ferramenta certa para o trabalho. o ostringstream é útil e flexível, mas há uma penalidade por usá-lo da maneira que você está tentando. char [] é um trabalho mais árduo, mas os ganhos de desempenho podem ser ótimos (lembre-se de que o gcc provavelmente incorporará os memcpys para você também).
Em resumo, o ostringstream não está quebrado, mas quanto mais você se aproxima do metal, mais rápido o seu código será executado. Assembler ainda tem vantagens para algumas pessoas.
fonte
ostringstream::write()
tem que fazer issovector::push_back()
não? Se for o caso, deve ser mais rápido, pois é entregue um bloco em vez de quatro elementos individuais. Seostringstream
for mais lento do questd::vector
sem fornecer nenhum recurso adicional, então eu chamaria isso de quebrado.stringbuf
diretamente não removerá todas as chamadas de função, poisstringbuf
a interface pública consiste em funções públicas não virtuais na classe base, que são despachadas para a função virtual protegida na classe derivada.sputn
função pública que chama o protegido virtualxsputn
, fosse incorporada. Mesmo quexsputn
não esteja embutido, o compilador pode, enquanto embutidosputn
, determinar axsputn
substituição exata necessária e gerar uma chamada direta sem passar pela vtable.Para obter melhor desempenho, você precisa entender como funcionam os contêineres que você está usando. No seu exemplo de matriz char [], a matriz do tamanho necessário é alocada previamente. No seu exemplo vetorial e ostringstream, você está forçando os objetos a alocar e realocar repetidamente e, possivelmente, copiar dados muitas vezes à medida que o objeto cresce.
Com std :: vector, isso é facilmente resolvido inicializando o tamanho do vetor para o tamanho final, como você fez na matriz char; em vez disso, você prejudica injustamente o desempenho, redimensionando para zero! Essa dificilmente é uma comparação justa.
Com relação ao ostringstream, não é possível pré-alocar o espaço, eu sugeriria que é um uso inadequado. A classe tem muito mais utilidade do que uma simples matriz de caracteres, 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. O C ++ fornece uma ampla variedade de contêineres e um ostringstram está entre os menos apropriados para essa finalidade.
No caso do vetor e do ostringstream, você obtém proteção contra saturação de buffer, não obtém isso com uma matriz char, e essa proteção não é gratuita.
fonte
ostringstream.str.reserve(4000000)
e não fez diferença.ostringstream
você pode "reservar" passando uma string fictícia, ou seja:ostringstream str(string(1000000 * sizeof(int), '\0'));
Comvector
, oresize
não desaloca nenhum espaço, ele só se expande se for necessário.vector[]
operador normalmente NÃO é verificado quanto a erros de limites por padrão.vector.at()
é no entanto.vector<T>::resize(0)
geralmente nãooperator[]
, maspush_back()
(por meio deback_inserter
), que definitivamente faz o teste de estouro. Adicionada outra versão que não usapush_back
.