Lo standard C ++ impone scarse prestazioni per iostreams o sto solo gestendo una scarsa implementazione?


197

Ogni volta che menziono le lente prestazioni degli iostreams della libreria standard C ++, mi viene incontro un'ondata di incredulità. Tuttavia ho risultati del profiler che mostrano grandi quantità di tempo speso nel codice della libreria iostream (ottimizzazioni complete del compilatore) e il passaggio da iostreams ad API I / O specifiche del sistema operativo e la gestione del buffer personalizzata offre un ordine di miglioramento della grandezza.

Quale lavoro aggiuntivo sta facendo la libreria standard C ++, è richiesta dallo standard ed è utile nella pratica? Oppure alcuni compilatori forniscono implementazioni di iostreams competitivi con la gestione manuale del buffer?

Punti di riferimenti

Per far muovere le cose, ho scritto un paio di brevi programmi per esercitare il buffering interno degli iostreams:

Si noti che le versioni ostringstreame stringbufeseguono meno iterazioni perché sono molto più lente.

Su ideone, ostringstreamè circa 3 volte più lento di std:copy+ back_inserter+ std::vectore circa 15 volte più lento dimemcpy a un buffer non elaborato. Ciò è coerente con la profilazione prima e dopo quando ho passato la mia vera applicazione al buffering personalizzato.

Questi sono tutti buffer in memoria, quindi la lentezza degli iostreams non può essere biasimata su I / O su disco lento, troppo flushing, sincronizzazione con stdio o qualsiasi altra cosa che le persone usano per scusare la lentezza osservata della libreria standard C ++ iostream.

Sarebbe bello vedere benchmark su altri sistemi e commenti su cose che fanno le comuni implementazioni (come libc ++ di gcc, Visual C ++, Intel C ++) e su quanto dell'overhead è richiesto dallo standard.

Razionale per questo test

Alcune persone hanno correttamente sottolineato che gli iostreams sono più comunemente usati per l'output formattato. Tuttavia, sono anche l'unica API moderna fornita dallo standard C ++ per l'accesso ai file binari. Ma il vero motivo per eseguire test delle prestazioni sul buffering interno si applica al tipico I / O formattato: se gli iostreams non riescono a mantenere il controller del disco fornito con dati non elaborati, come possono tenere il passo anche quando sono responsabili della formattazione?

Tempistica di riferimento

Tutti questi sono per iterazione dell'esterno (k ciclo ).

Su ideone (gcc-4.3.4, SO e hardware sconosciuti):

  • ostringstream: 53 millisecondi
  • stringbuf: 27 ms
  • vector<char> e back_inserter : 17,6 ms
  • vector<char> con iteratore ordinario: 10,6 ms
  • vector<char> controllo iteratore e limiti: 11,4 ms
  • char[]: 3,7 ms

Sul mio laptop (Visual C ++ 2010 x86, cl /Ox /EHscWindows 7 Ultimate a 64 bit, Intel Core i7, 8 GB RAM):

  • ostringstream: 73,4 millisecondi, 71,6 ms
  • stringbuf: 21,7 ms, 21,3 ms
  • vector<char>e back_inserter: 34,6 ms, 34,4 ms
  • vector<char> con iteratore ordinario: 1,10 ms, 1,04 ms
  • vector<char> controllo iteratore e limiti: 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 x 86, con profilo-Guided Optimization cl /Ox /EHsc /GL /c, link /ltcg:pgi, corsa, link /ltcg:pgo, misura:

  • ostringstream: 61,2 ms, 60,5 ms
  • vector<char> con iteratore ordinario: 1,04 ms, 1,03 ms

Stesso laptop, stesso sistema operativo, utilizzando cygwin gcc 4.3.4 g++ -O3:

  • ostringstream: 62,7 ms, 60,5 ms
  • stringbuf: 44,4 ms, 44,5 ms
  • vector<char>e back_inserter: 13,5 ms, 13,6 ms
  • vector<char> con iteratore ordinario: 4,1 ms, 3,9 ms
  • vector<char> controllo iteratore e limiti: 4.0 ms, 4.0 ms
  • char[]: 3,57 ms, 3,75 ms

Stesso computer portatile, Visual C ++ 2008 SP1, cl /Ox /EHsc:

  • ostringstream: 88,7 ms, 87,6 ms
  • stringbuf: 23,3 ms, 23,4 ms
  • vector<char>e back_inserter: 26,1 ms, 24,5 ms
  • vector<char> con iteratore ordinario: 3,13 ms, 2,48 ms
  • vector<char> controllo iteratore e limiti: 2,97 ms, 2,53 ms
  • char[]: 1,52 ms, 1,25 ms

Stesso laptop, compilatore Visual C ++ 2010 a 64 bit:

  • ostringstream: 48,6 ms, 45,0 ms
  • stringbuf: 16,2 ms, 16,0 ms
  • vector<char>e back_inserter: 26,3 ms, 26,5 ms
  • vector<char> con iteratore ordinario: 0,87 ms, 0,89 ms
  • vector<char> controllo iteratore e limiti: 0,99 ms, 0,99 ms
  • char[]: 1,25 ms, 1,24 ms

EDIT: ho eseguito tutto due volte per vedere quanto erano coerenti i risultati. IMO abbastanza coerente.

NOTA: sul mio laptop, poiché posso risparmiare più tempo della CPU di quanto ideone consenta, ho impostato il numero di iterazioni su 1000 per tutti i metodi. Ciò significa che ostringstreame la vectorriallocazione, che ha luogo solo al primo passaggio, dovrebbe avere un impatto limitato sui risultati finali.

EDIT: Oops, vectorho trovato un bug nell'iteratore -con-ordinario, l'iteratore non era avanzato e quindi c'erano troppi hit della cache. Mi chiedevo come vector<char>fosse meglio char[]. Tuttavia non ha fatto molta differenza, vector<char>è ancora più veloce dichar[] VC ++ 2010.

conclusioni

Il buffering dei flussi di output richiede tre passaggi ogni volta che i dati vengono aggiunti:

  • Verificare che il blocco in entrata si adatti allo spazio buffer disponibile.
  • Copia il blocco in arrivo.
  • Aggiorna il puntatore di fine dati.

L'ultimo frammento di codice che ho pubblicato, " vector<char>semplice iteratore più controllo dei limiti" non solo fa questo, ma alloca spazio aggiuntivo e sposta i dati esistenti quando il blocco in arrivo non si adatta. Come ha sottolineato Clifford, il buffering in una classe I / O di file non dovrebbe farlo, semplicemente svuoterebbe il buffer corrente e lo riutilizzerebbe. Quindi questo dovrebbe essere un limite superiore al costo dell'output di buffering. Ed è esattamente ciò che è necessario per creare un buffer in memoria funzionante.

Allora perché stringbuf2,5 volte più lento su ideone e almeno 10 volte più lento quando lo collaudo? Non viene utilizzato polimorficamente in questo semplice micro-benchmark, quindi non lo spiega.


24
Stai scrivendo un milione di caratteri uno alla volta e ti chiedi perché sia ​​più lento rispetto alla copia in un buffer preallocato?
Anon.

20
@Anon: sto bufferizzando quattro milioni di byte quattro alla volta, e sì, mi chiedo perché sia ​​lento. Se std::ostringstreamnon è abbastanza intelligente da aumentare esponenzialmente le dimensioni del buffer come std::vectorfa, questo è (A) stupido e (B) qualcosa a cui le persone che pensano alle prestazioni I / O dovrebbero pensare. Ad ogni modo, il buffer viene riutilizzato, non viene riallocato ogni volta. E std::vectorsta anche usando un buffer in crescita dinamica. Sto cercando di essere onesto qui.
Ben Voigt,

14
Quale compito stai effettivamente cercando di confrontare? Se non stai utilizzando nessuna delle funzionalità di formattazione di ostringstreame desideri prestazioni il più veloci possibile, allora dovresti considerare di andare direttamente a stringbuf. Si ostreamsuppone che le classi colleghino la funzionalità di formattazione consapevole delle impostazioni locali con una scelta flessibile del buffer (file, stringa, ecc.) rdbuf()E la sua interfaccia di funzione virtuale. Se non stai eseguendo alcuna formattazione, quel livello extra di indiretta sembrerà sicuramente proporzionalmente costoso rispetto ad altri approcci.
CB Bailey,

5
+1 per la verità op. Abbiamo ottenuto accelerazioni di ordine o grandezza passando da ofstreama fprintfquando si trasmettono informazioni di registrazione che coinvolgono i doppi. MSVC 2008 su WinXPsp3. iostreams è solo un cane lento.
KitsuneYMG,

6
Ecco alcuni test sul sito della commissione: open-std.org/jtc1/sc22/wg21/docs/D_5.cpp
Johannes Schaub - litb

Risposte:


49

Non rispondendo alle specifiche della tua domanda tanto quanto al titolo: il Rapporto tecnico del 2006 sulle prestazioni del C ++ ha una sezione interessante su IOStreams (p.68). Il più rilevante per la tua domanda è nella Sezione 6.1.2 ("Velocità di esecuzione"):

Poiché alcuni aspetti dell'elaborazione di IOStreams sono distribuiti su più facce, sembra che lo Standard imponga un'implementazione inefficiente. Ma non è così: usando una qualche forma di preelaborazione, gran parte del lavoro può essere evitato. Con un linker leggermente più intelligente di quello che viene normalmente utilizzato, è possibile rimuovere alcune di queste inefficienze. Questo è discusso in §6.2.3 e §6.2.5.

Da quando il rapporto è stato scritto nel 2006, si spera che molte delle raccomandazioni siano state incorporate negli attuali compilatori, ma forse non è così.

Come hai detto, le sfaccettature potrebbero non apparire in write()(ma non lo presumo ciecamente). Quindi che cosa caratterizza? L'esecuzione di GProf sul ostringstreamcodice compilato con GCC fornisce la seguente suddivisione:

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

Quindi trascorre gran parte del tempo xsputn, che alla fine chiama std::copy()dopo un sacco di controllo e aggiornamento delle posizioni del cursore e dei buffer (dare un'occhiata c++\bits\streambuf.tccai dettagli).

La mia opinione su questo è che ti sei concentrato sulla situazione peggiore. Tutto il controllo eseguito sarebbe una piccola parte del lavoro totale svolto se si trattasse di blocchi di dati ragionevolmente grandi. Ma il tuo codice sta spostando i dati in quattro byte alla volta e incorrendo tutti i costi aggiuntivi ogni volta. Chiaramente uno eviterebbe di farlo in una situazione di vita reale - considera quanto trascurabile la penalità sarebbe stata se writefosse stata chiamata su un array di 1 mts anziché su 1 m volte su un int. E in una situazione di vita reale si apprezzerebbero davvero le importanti caratteristiche di IOStreams, vale a dire il suo design sicuro per la memoria e il tipo. Tali vantaggi hanno un prezzo e hai scritto un test che fa sì che questi costi dominino i tempi di esecuzione.


Sembra un'ottima informazione per una futura domanda sulle prestazioni di inserimento / estrazione formattata di iostreams che probabilmente chiederò presto. Ma non credo ci siano sfaccettature coinvolte ostream::write().
Ben Voigt,

4
+1 per la profilazione (è una macchina Linux presumo?). Tuttavia, in realtà sto aggiungendo quattro byte alla volta (in realtà sizeof i, ma tutti i compilatori con cui sto testando hanno 4 byte int). E questo non mi sembra affatto irrealistico, a quali dimensioni pensi che vengano passate in ogni chiamata xsputnin un tipico codice come stream << "VAR: " << var.x << ", " << var.y << endl;.
Ben Voigt,

39
@beldaz: quell'esempio di codice "tipico" che chiama solo xsputncinque volte potrebbe benissimo essere all'interno di un ciclo che scrive un file di 10 milioni di righe. Passare i dati agli iostreams in grandi blocchi è molto meno uno scenario di vita reale rispetto al mio codice di riferimento. Perché dovrei scrivere su uno stream con buffer con il numero minimo di chiamate? Se devo fare il mio buffering, qual è il punto di iostreams comunque? E con i dati binari, ho la possibilità di bufferizzarlo da solo, quando scrivo milioni di numeri in un file di testo, l'opzione bulk non esiste, DEVO chiamare operator <<per ognuno.
Ben Voigt,

1
@beldaz: si può stimare quando l'I / O inizia a dominare con un semplice calcolo. Con una velocità di scrittura media di 90 MB / s, tipica degli attuali dischi rigidi di fascia consumer, lo svuotamento del buffer da 4 MB richiede <45 ms (la velocità effettiva, la latenza non è importante a causa della cache di scrittura del sistema operativo). Se l'esecuzione del loop interno impiega più tempo a riempire il buffer, la CPU sarà il fattore limitante. Se il ciclo interno funziona più velocemente, allora l'I / O sarà il fattore limitante, o almeno rimane un po 'di tempo della CPU per fare il vero lavoro.
Ben Voigt,

5
Naturalmente, ciò non significa che l'utilizzo di iostreams significhi necessariamente un programma lento. Se l'I / O è una parte molto piccola del programma, l'utilizzo di una libreria I / O con scarse prestazioni non avrà un impatto complessivo. Ma non essere chiamato abbastanza spesso da importare non è lo stesso di una buona prestazione, e nelle applicazioni pesanti di I / O, importa.
Ben Voigt,

27

Sono piuttosto deluso dagli utenti di Visual Studio là fuori, che piuttosto hanno avuto un bacio su questo:

  • Nell'implementazione di Visual Studio di ostream, l' sentryoggetto (che è richiesto dallo standard) entra in una sezione critica che protegge streambuf(che non è richiesto). Questo non sembra essere facoltativo, quindi paghi il costo della sincronizzazione del thread anche per uno stream locale utilizzato da un singolo thread, che non ha bisogno di sincronizzazione.

Questo fa male al codice che usa ostringstreamper formattare i messaggi piuttosto severamente. L' stringbufuso di evita direttamente l'uso di sentry, ma gli operatori di inserimento formattati non possono lavorare direttamente su streambufs. Per Visual C ++ 2010, la sezione critica sta rallentando ostringstream::writedi un fattore tre rispetto alla stringbuf::sputnchiamata sottostante .

Guardando i dati del profiler di beldaz su newlib , sembra chiaro che gcc sentrynon fa nulla di folle come questo. ostringstream::writesotto gcc richiede solo circa il 50% in più rispetto a stringbuf::sputn, ma di per stringbufsé è molto più lento di sotto VC ++. Ed entrambi sono ancora molto sfavorevoli rispetto all'utilizzo di un vector<char>buffer di I / O, sebbene non con lo stesso margine di VC ++.


Queste informazioni sono ancora aggiornate? L'implementazione di AFAIK, C ++ 11 fornita con GCC esegue questo blocco "pazzo". Certamente anche VS2010 lo fa ancora. Qualcuno potrebbe chiarire questo comportamento e se "che non è richiesto" è ancora valido in C ++ 11?
mloskot,

2
@mloskot: Non vedo alcun requisito di sicurezza del thread su sentry... "Il sentry di classe definisce una classe che è responsabile dell'esecuzione di operazioni con prefisso e suffisso sicuri delle eccezioni." e una nota "Il costruttore e il distruttore di sentinella possono anche eseguire ulteriori operazioni dipendenti dall'implementazione." Si può anche supporre dal principio C ++ di "non pagare per ciò che non si utilizza" che il comitato C ++ non approverebbe mai un requisito così dispendioso. Non esitate a fare una domanda sulla sicurezza del filo iostream.
Ben Voigt,

8

Il problema che vedi è nell'overhead di ogni chiamata a write (). Ogni livello di astrazione che aggiungi (char [] -> vector -> string -> ostringstream) aggiunge qualche altra chiamata / return di funzione e altre funzioni di pulizia che, se la chiami un milione di volte, si sommano.

Ho modificato due degli esempi su ideone per scrivere dieci in una volta. Il tempo ostringstream è passato da 53 a 6 ms (quasi 10 volte il miglioramento) mentre il loop dei caratteri è migliorato (da 3,7 a 1,5) - utile, ma solo di un fattore due.

Se sei così preoccupato per le prestazioni, allora devi scegliere lo strumento giusto per il lavoro. ostringstream è utile e flessibile, ma c'è una penalità per usarlo nel modo in cui stai cercando. char [] è un lavoro più duro, ma i guadagni in termini di prestazioni possono essere eccezionali (ricorda che gcc probabilmente incorporerà anche i memcpys per te).

In breve, ostringstream non è rotto, ma più ti avvicini al metal più veloce sarà il tuo codice. L'assemblatore ha ancora vantaggi per alcune persone.


8
Cosa ostringstream::write()deve fare che vector::push_back()no? Semmai, dovrebbe essere più veloce in quanto ha un blocco invece di quattro singoli elementi. Se ostringstreamè più lento che std::vectorsenza fornire funzionalità aggiuntive, allora sì, lo definirei rotto.
Ben Voigt,

1
@Ben Voigt: Al contrario, è qualcosa che il vettore deve fare che ostringstream NON deve fare per rendere il vettore più performante in questo caso. Il vettore è garantito per essere contiguo nella memoria, mentre ostringstream no. Vector è una delle classi progettate per essere performanti, mentre ostringstream no.
Dragontamer5788,

2
@Ben Voigt: l'utilizzo stringbufdiretto non rimuoverà tutte le chiamate di funzione poiché stringbufl'interfaccia pubblica è costituita da funzioni pubbliche non virtuali nella classe base che inviano quindi alla funzione virtuale protetta nella classe derivata.
CB Bailey,

2
@Charles: su qualsiasi compilatore decente dovrebbe, poiché la chiamata di funzione pubblica verrà inserita in un contesto in cui il tipo dinamico è noto al compilatore, può rimuovere l'indirizzamento indiretto e persino incorporare quelle chiamate.
Ben Voigt,

6
@Roddy: dovrei pensare che questo è tutto il codice modello in linea, visibile in ogni unità di compilazione. Ma immagino che potrebbe variare in base all'implementazione. Di sicuro mi aspetterei che la chiamata in discussione, la sputnfunzione pubblica che chiama il virtuale protetto xsputn, sia integrata. Anche se xsputnnon è in linea, il compilatore può, durante l'inline sputn, determinare l'esatta xsputnsostituzione necessaria e generare una chiamata diretta senza passare attraverso la vtable.
Ben Voigt,

1

Per ottenere prestazioni migliori devi capire come funzionano i contenitori che stai utilizzando. Nell'esempio di array char [], l'array della dimensione richiesta viene allocato in anticipo. Nel tuo esempio vettoriale e ostringstream stai forzando gli oggetti a allocare e riallocare ripetutamente e possibilmente copiare i dati molte volte man mano che l'oggetto cresce.

Con std :: vector questo si risolve facilmente inizializzando la dimensione del vettore alla dimensione finale come hai fatto con l'array char; invece paralizzi ingiustamente la performance ridimensionandola a zero! Questo non è certo un confronto equo.

Per quanto riguarda ostringstream, non è possibile preallocare lo spazio, suggerirei che è un uso inappropriato. La classe ha un'utilità di gran lunga maggiore di un semplice array di caratteri, ma se non hai bisogno di quell'utilità, non utilizzarla, perché in ogni caso pagherai l'overhead. Invece dovrebbe essere usato per quello che serve a - formattare i dati in una stringa. C ++ offre una vasta gamma di contenitori e uno ostringstram è tra i meno appropriati per questo scopo.

Nel caso di vector e ostringstream ottieni protezione da sovraccarico del buffer, non lo ottieni con un array di caratteri e tale protezione non viene fornita gratuitamente.


1
L'allocazione non sembra essere il problema di ostringstream. Cerca solo a zero per le successive iterazioni. Nessun troncamento. Inoltre ho provato ostringstream.str.reserve(4000000)e non ha fatto differenza.
Roddy,

Penso ostringstreamche potresti "riservare" passando una stringa fittizia, ovvero: ostringstream str(string(1000000 * sizeof(int), '\0'));con vector, resizenon si occupa di allocare alcuno spazio, si espande solo se necessario.
Nim,

1
msgstr "vector .. protezione da sovraccarico del buffer". Un malinteso comune - l' vector[]operatore NON è in genere controllato per errori di limiti per impostazione predefinita. vector.at()lo è comunque.
Roddy,

2
vector<T>::resize(0)di solito non rialloca la memoria
Niki Yoshiuchi il

2
@Roddy: non usando operator[], ma push_back()(a titolo di back_inserter), che sicuramente fa il test di overflow. Aggiunta un'altra versione che non utilizza push_back.
Ben Voigt,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.