194 votes

Ne la piètre performance de C++ standard mandat pour iostreams, ou suis-je juste face à une mauvaise mise en œuvre ?

Chaque fois que je mentionne ralentir les performances de C++ standard library iostreams, je reçois rencontré une vague d'incrédulité. Pourtant, j'ai profiler les résultats montrent de grandes quantités de temps passé dans la bibliothèque iostream code (optimisations du compilateur complet), et de passer d'iostreams OS-I/O Api et personnalisé à la gestion des tampons donne un ordre de grandeur de l'amélioration.

Ce travail supplémentaire est la norme C++ bibliothèque à faire, est-il requis par la norme, et est-il utile dans la pratique? Ou faire un peu de compilateurs fournir des implémentations de iostreams qui entrent en concurrence avec le manuel de gestion de tampon?

Repères

Pour obtenir les questions de déplacement, j'ai écrit un couple de programmes courts à l'exercice de la iostreams interne de mise en mémoire tampon:

Notez que l' ostringstream et stringbuf versions exécuter en moins d'itérations, car ils sont beaucoup plus lent.

Sur ideone, l' ostringstream est environ 3 fois plus lent que l' std:copy + back_inserter + std::vector, et 15 fois plus lent que l' memcpy en raw d'un tampon. C'est cohérent avec avant-et-après profilage quand j'ai changé mon véritable application personnalisée pour la mise en mémoire tampon.

Ces sont tous en mémoire tampon, de sorte que la lenteur des iostreams ne peut pas être blâmé sur les e/S disque lente, trop de rinçage, la synchronisation avec stdio, ou toutes les autres choses que les gens utilisent pour excuse observé la lenteur de la norme C++ de la bibliothèque iostream.

Il serait agréable de voir des tests sur d'autres systèmes et des commentaires sur des choses communes des implémentations ne (comme gcc de la libc++, Visual C++, Intel C++) et comment une grande partie de la charge est mandaté par le standard.

Justification pour ce test

Un certain nombre de personnes ont fait remarquer à juste titre que iostreams sont plus couramment utilisés pour une sortie formatée. Cependant, ils sont également les seuls moderne API fournie par la norme C++ pour l'accès aux fichiers binaires. Mais la vraie raison pour faire des tests de performance sur les tampons internes s'applique à la typique formaté I/O: si iostreams ne pouvez pas garder le contrôleur de disque fourni avec les données brutes, comment peuvent-ils rester en place lorsqu'ils sont responsables de la mise en forme ainsi?

Référence Timing

Tous ces éléments sont par itération de l'extérieur (k) en boucle.

Sur ideone (gcc-4.3.4 inconnu de l'OS et du matériel):

  • ostringstream: 53 millisecondes
  • stringbuf: 27 ms
  • vector<char> et back_inserter: 17.6 ms
  • vector<char> ordinaire itérateur: 10.6 ms
  • vector<char> itérateur et de vérification de limites: 11.4 ms
  • char[]: 3.7 ms

Sur mon portable (Visual C++ 2010 x86, cl /Ox /EHsc, Windows 7 Ultime 64 bits, Intel Core i7, 8 GO de RAM):

  • ostringstream: 73.4 millisecondes, 71.6 ms
  • stringbuf: 21.7 ms, 21.3 ms
  • vector<char> et back_inserter: 34.6 ms, de 34,4 ms
  • vector<char> ordinaire itérateur: 1.10 ms, 1.04 ms
  • vector<char> itérateur et de vérification de limites: 1.11 ms, 0.87 ms, 1.12 ms, de 0,89 ms, 1.02 ms, 1.14 ms
  • char[]: 1.48 ms, ms 1.57

Visual C++ 2010 x86, avec Profil de l'Optimisation orientée cl /Ox /EHsc /GL /c, link /ltcg:pgi, exécuter, link /ltcg:pgo, mesure:

  • ostringstream: 61.2 ms, ms 60.5
  • vector<char> ordinaire itérateur: 1.04 ms, ms 1.03

Même l'ordinateur portable, le même système d'exploitation, l'utilisation de cygwin gcc 4.3.4 g++ -O3:

  • ostringstream: 62.7 ms, ms 60.5
  • stringbuf: 44.4 ms, ms 44.5
  • vector<char> et back_inserter: 13.5 ms, 13.6 ms
  • vector<char> ordinaire itérateur: 4.1 ms, de 3,9 ms
  • vector<char> itérateur et de vérification de limites: 4.0 ms, 4.0 ms
  • char[]: 3.57 ms, 3.75 ms

Même ordinateur portable, Visual C++ 2008 SP1, cl /Ox /EHsc:

  • ostringstream: 88.7 ms, ms 87.6
  • stringbuf: 23.3 ms, de 23,4 ms
  • vector<char> et back_inserter: 26.1 ms, ms 24.5
  • vector<char> ordinaire itérateur: 3.13 ms, 2.48 ms
  • vector<char> itérateur et de vérification de limites: 2.97 ms, 2.53 ms
  • char[]: 1.52 ms, 1,25 ms

Même ordinateur portable, Visual C++ 2010 compilateur 64 bits:

  • ostringstream: 48.6 ms, 45.0 ms
  • stringbuf: 16.2 ms, 16.0 ms
  • vector<char> et back_inserter: 26.3 ms, ms 26.5
  • vector<char> ordinaire itérateur: 0.87 ms, de 0,89 ms
  • vector<char> itérateur et de vérification de limites: 0.99 ms, ms 0.99
  • char[]: 1,25 ms, 1.24 ms

EDIT: toutes Couru deux fois pour voir le degré de cohérence ont été les résultats. Assez cohérente de l'OMI.

NOTE: Sur mon portable, depuis que je peux consacrer davantage de temps CPU que ideone permet, j'ai mis le nombre d'itérations pour 1000 pour l'ensemble des méthodes. Cela signifie qu' ostringstream et vector réaffectation, qui n'a lieu que lors de la première passe, devrait avoir peu d'impact sur le résultat final.

EDIT: Oups, a trouvé un bug dans l' vector-avec-ordinaire-itérateur, l'itérateur n'a pas été avancé et donc il y avait trop de cache. Je me demandais comment vector<char> a été surclassé char[]. Il n'a pas beaucoup de différence si, vector<char> est encore plus rapide qu' char[] sous VC++ 2010.

Conclusions

Mise en mémoire tampon du flux de sortie nécessite trois étapes chaque fois que des données sont présentés en annexe:

  • Vérifiez que le nouveau bloc s'adapte à la disposition de l'espace tampon.
  • Copie entrant bloc.
  • Mise à jour de la fin-de-pointeur de données.

Le dernier extrait de code que j'ai posté, "vector<char> simple itérateur plus de limites cochez la case" non seulement cela, il alloue également de plus d'espace et déplace les données existantes lorsque les entrants bloc ne rentre pas. Comme Clifford souligné, la mise en mémoire tampon dans un fichier I/O de la classe à ne pas avoir à le faire, il suffit de vider le buffer courant et de le réutiliser. Donc, cela devrait être une limite supérieure sur le coût de mise en mémoire tampon de sortie. Et c'est exactement ce qui est nécessaire pour faire un travail en mémoire tampon.

Alors, pourquoi est - stringbuf 2,5 x plus lent sur ideone, et au moins 10 fois plus lent quand je l'ai tester? Il n'est pas utilisé polymorphically dans ce simple micro-benchmark, afin que ne l'explique pas.

48voto

beldaz Points 1432

Ne pas répondre aux spécificités de votre question tant que le titre: 2006 Rapport Technique sur les Performances de C++ a une section intéressante sur IOStreams (p.68). Les plus pertinents à votre question est dans la Section 6.1.2 ("Vitesse d'Exécution"):

Depuis certains aspects de IOStreams de traitement sont distribué sur plusieurs facettes, il semble que la Norme impose l' inefficace mise en œuvre. Mais ce n'est pas le cas en utilisant une forme de prétraitement, une grande partie du travail peut être évitée. Avec un peu plus intelligent l'éditeur de liens que ce qui est généralement utilisé, il est possible de supprimer certains de ces les inefficacités. Cette question est discutée dans §6.2.3 et §6.2.5.

Depuis que le rapport a été écrit en 2006 on pourrait espérer que de nombreuses recommandations ont été incorporés dans les compilateurs, mais peut-être que ce n'est pas le cas.

Comme vous le mentionnez, les facettes peuvent pas, en write() (mais je ne voudrais pas présumer que l'aveuglette). Donc, de quoi? L'exécution de GProf sur votre ostringstream code compilé avec GCC donne la répartition suivante:

  • 44.23% en std::basic_streambuf<char>::xsputn(char const*, int)
  • 34.62% en std::ostream::write(char const*, int)
  • 12.50% en main
  • 6.73% en std::ostream::sentry::sentry(std::ostream&)
  • 0.96% std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 0.96% std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • 0.00% std::fpos<int>::fpos(long long)

Si la majeure partie du temps est passé en xsputn, ce qui a finalement appels std::copy() , après beaucoup de vérification et mise à jour de la position du curseur et les tampons (avoir un coup d'oeil en c++\bits\streambuf.tcc pour les détails).

De mon point de vue sur ce que vous ayez mis l'accent sur le pire cas. La vérification est effectuée, une petite fraction de la quantité totale de travail fait si nous avions affaire à raisonnablement de gros blocs de données. Mais votre code est en train de passer des données dans quatre octets à la fois, et en assumant tous les frais supplémentaires chaque fois. Clairement on éviter de le faire dans une situation de vie réelle - examiner comment négligeable de la pénalité aurait été si write a été appelée sur un tableau de 1m ints au lieu de 1m de fois sur un int. Et dans une situation de vie réelle serait vraiment apprécier les caractéristiques importantes de IOStreams, à savoir sa mémoire et de type-safe design. Ces avantages ont un prix, et vous avez écrit un test qui fait de ces coûts de dominer le temps d'exécution.

27voto

Ben Voigt Points 151460

Je suis plutôt déçu dans le aux utilisateurs de Visual Studio, qui avait plutôt un gimme sur celui-ci:

  • Dans Visual Studio, la mise en œuvre de l' ostream, sentry objet (qui est requis par la norme) entre dans une section critique la protection de l' streambuf (ce qui n'est pas nécessaire). Cela ne semble pas être en option, de sorte que vous payez le coût de la synchronisation des threads, même pour un cours d'eau local utilisé que par un seul thread, qui n'a pas besoin de synchronisation.

Cela fait mal de code qui utilise ostringstream pour le format des messages assez sévèrement. À l'aide de l' stringbuf directement évite l'utilisation d' sentry, mais la mise en forme des opérateurs d'insertion ne peut pas travailler directement sur streambufs. Pour Visual C++ 2010, la section critique est de ralentir ostringstream::write par un facteur trois par rapport au sous-jacent stringbuf::sputn appel.

En regardant beldaz du profileur de données sur newlib, il semble clair que du ccg sentry ne pas faire quelque chose de fou comme ça. ostringstream::write sous gcc prend seulement environ 50% de plus de stringbuf::sputn, mais stringbuf elle-même est beaucoup plus lent que sous VC++. Et tous les deux se comparent très favorablement à l'aide d'un vector<char> pour les I/O tampon, mais pas par la même marge que sous VC++.

8voto

Roddy Points 32503

Le problème que vous voyez est tous dans la surcharge autour de chaque appel à write(). Chaque niveau d'abstraction que vous ajoutez (char[] -> vector -> string -> ostringstream) ajoute un peu plus de l'appel de fonction/retours et d'autres de ménage guff que - si vous appelez ça un million de fois - ajoute.

J'ai modifié deux des exemples sur ideone d'écrire dix entiers à la fois. Le ostringstream temps est passé de 53 à 6 ms (près de 10 x amélioration), tandis que le char de la boucle de l'amélioration (de 3,7 à 1.5) - utile, mais seulement par un facteur de deux.

Si vous êtes concerné sur les performances, alors vous avez besoin pour choisir le bon outil pour le travail. ostringstream est utile et flexible, mais il y a une pénalité pour l'utiliser de la manière que vous essayez de. char[] est beaucoup plus dur, mais les gains de performance peut être grande (souvenez-vous de la gcc sera probablement inline l'memcpys pour vous aussi).

En bref, ostringstream n'est pas rompu, mais plus vous vous rapprochez du métal le plus rapide de votre code sera exécuté. L'assembleur a encore des avantages pour certains folk.

1voto

Clifford Points 29933

Pour obtenir de meilleures performances, vous devez comprendre comment les conteneurs que vous utilisez sont de travail. Dans votre char tableau[] exemple, le tableau de la taille requise est alloué à l'avance. Dans votre vecteur et ostringstream exemple, vous forcez les objets à plusieurs reprises de les affecter et réaffecter et, éventuellement, la copie de données autant de fois que l'objet se développe.

Avec std::vector c'est facilement résolu par l'initialisation de la taille du vecteur de la taille finale que vous avez fait le char tableau; au lieu de cela vous plutôt injustement paralyser l'exécution par le redimensionnement à zéro! Ça n'est pas une comparaison équitable.

À l'égard de ostringstream, preallocating l'espace n'est pas possible, je dirais que c'est un inappropruate utilisation. La classe a beaucoup plus d'utilité qu'un simple tableau de char, mais si vous n'avez pas besoin de l'utilitaire, puis ne pas utiliser, parce que vous allez payer la surcharge en tout cas. Il devrait plutôt être utilisé pour ce qu'elle est bonne pour la mise en forme des données dans une chaîne de caractères. C++ offre une vaste gamme de conteneurs et d'un ostringstram est parmi les moins appropriés à cette fin.

Dans le cas du vecteur et ostringstream vous obtenez une protection de dépassement de la mémoire tampon, vous n'avez pas le cas avec un tableau de char, et que la protection ne sont pas gratuits.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X