Chaque fois que je mentionne la lenteur des performances des iostreams de la bibliothèque standard C ++, je reçois une vague d'incrédulité. Pourtant, j'ai des résultats de profileur montrant de grandes quantités de temps passé dans le code de la bibliothèque iostream (optimisations complètes du compilateur), et le passage d'iostreams à des API d'E / S spécifiques au système d'exploitation et à la gestion de la mémoire tampon personnalisée donne une amélioration de l'ordre de grandeur.
Quel travail supplémentaire la bibliothèque standard C ++ fait-elle, est-elle requise par la norme et est-elle utile dans la pratique? Ou certains compilateurs fournissent-ils des implémentations d'iostreams qui sont compétitives avec la gestion manuelle des tampons?
Repères
Pour faire avancer les choses, j'ai écrit quelques programmes courts pour exercer la mise en mémoire tampon interne iostreams:
- mettre des données binaires dans un
ostringstream
http://ideone.com/2PPYw - mettre les données binaires dans un
char[]
tampon http://ideone.com/Ni5ct - mettre des données binaires dans un
vector<char>
utilisantback_inserter
http://ideone.com/Mj2Fi - NOUVEAU :
vector<char>
itérateur simple http://ideone.com/9iitv - NOUVEAU : mettre des données binaires directement dans
stringbuf
http://ideone.com/qc9QA - NOUVEAU :
vector<char>
simple itérateur et vérification des limites http://ideone.com/YyrKy
Notez que les versions ostringstream
et stringbuf
exécutent moins d'itérations car elles sont beaucoup plus lentes.
Sur ideone, le ostringstream
est environ 3 fois plus lent que std:copy
+ back_inserter
+ std::vector
, et environ 15 fois plus lent que memcpy
dans un tampon brut. Cela semble cohérent avec le profilage avant et après lorsque j'ai basculé ma véritable application vers une mise en mémoire tampon personnalisée.
Ce sont tous des tampons en mémoire, donc la lenteur des iostreams ne peut pas être imputée à des E / S de disque lentes, trop de vidage, de synchronisation avec stdio, ou toute autre chose que les gens utilisent pour excuser la lenteur observée de la bibliothèque standard C ++ iostream.
Ce serait bien de voir des repères sur d'autres systèmes et des commentaires sur les choses que font les implémentations courantes (telles que la libc ++ de gcc, Visual C ++, Intel C ++) et combien de la surcharge est mandatée par la norme.
Justification de ce test
Un certain nombre de personnes ont correctement souligné que les flux ios sont plus couramment utilisés pour la sortie formatée. Cependant, ils sont également la seule API moderne fournie par la norme C ++ pour l'accès aux fichiers binaires. Mais la vraie raison de faire des tests de performances sur la mise en mémoire tampon interne s'applique aux E / S formatées typiques: si les iostreams ne peuvent pas maintenir le contrôleur de disque alimenté en données brutes, comment peuvent-ils éventuellement suivre lorsqu'ils sont également responsables du formatage?
Calendrier de référence
Tous ces éléments sont par itération de la k
boucle externe ( ).
Sur ideone (gcc-4.3.4, OS et matériel inconnus):
ostringstream
: 53 millisecondesstringbuf
: 27 msvector<char>
etback_inserter
: 17,6 msvector<char>
avec itérateur ordinaire: 10,6 msvector<char>
vérification de l'itérateur et des limites: 11,4 mschar[]
: 3,7 ms
Sur mon ordinateur portable (Visual C ++ 2010 x86,, cl /Ox /EHsc
Windows 7 Ultimate 64 bits, Intel Core i7, 8 Go de RAM):
ostringstream
: 73,4 millisecondes, 71,6 msstringbuf
: 21,7 ms, 21,3 msvector<char>
etback_inserter
: 34,6 ms, 34,4 msvector<char>
avec itérateur ordinaire: 1,10 ms, 1,04 msvector<char>
vérification de l'itérateur et des 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 x86, avec l' optimisation du profil guidée cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, course, link /ltcg:pgo
, mesure:
ostringstream
: 61,2 ms, 60,5 msvector<char>
avec itérateur ordinaire: 1,04 ms, 1,03 ms
Même ordinateur portable, même système d'exploitation, utilisant cygwin gcc 4.3.4 g++ -O3
:
ostringstream
: 62,7 ms, 60,5 msstringbuf
: 44,4 ms, 44,5 msvector<char>
etback_inserter
: 13,5 ms, 13,6 msvector<char>
avec itérateur ordinaire: 4,1 ms, 3,9 msvector<char>
vérification de l'itérateur et des limites: 4,0 ms, 4,0 mschar[]
: 3,57 ms, 3,75 ms
Ordinateur portable même, Visual C ++ 2008 SP1, cl /Ox /EHsc
:
ostringstream
: 88,7 ms, 87,6 msstringbuf
: 23,3 ms, 23,4 msvector<char>
etback_inserter
: 26,1 ms, 24,5 msvector<char>
avec itérateur ordinaire: 3,13 ms, 2,48 msvector<char>
vérification de l'itérateur et des limites: 2,97 ms, 2,53 mschar[]
: 1,52 ms, 1,25 ms
Même ordinateur portable, compilateur Visual C ++ 2010 64 bits:
ostringstream
: 48,6 ms, 45,0 msstringbuf
: 16,2 ms, 16,0 msvector<char>
etback_inserter
: 26,3 ms, 26,5 msvector<char>
avec itérateur ordinaire: 0,87 ms, 0,89 msvector<char>
vérification de l'itérateur et des limites: 0,99 ms, 0,99 mschar[]
: 1,25 ms, 1,24 ms
EDIT: Ran tous deux fois pour voir la cohérence des résultats. IMO assez cohérent.
REMARQUE: sur mon ordinateur portable, comme je peux épargner plus de temps processeur que ne le permet ideone, j'ai défini le nombre d'itérations sur 1000 pour toutes les méthodes. Cela signifie que ostringstream
et la vector
réaffectation, qui se déroule uniquement sur la première passe, devrait avoir peu d' impact sur les résultats finaux.
EDIT: Oups, a trouvé un bogue dans l' vector
itérateur -with-ordinaire, l'itérateur n'était pas avancé et donc il y avait trop de hits de cache. Je me demandais comment vector<char>
était surperformant char[]
. Cela n'a pas fait beaucoup de différence cependant, vector<char>
est toujours plus rapide quechar[]
sous VC ++ 2010.
Conclusions
La mise en mémoire tampon des flux de sortie nécessite trois étapes chaque fois que des données sont ajoutées:
- Vérifiez que le bloc entrant correspond à l'espace tampon disponible.
- Copiez le bloc entrant.
- Mettez à jour le pointeur de fin de données.
Le dernier extrait de code que j'ai publié, " vector<char>
simple itérateur et vérification des limites", non seulement cela, il alloue également de l'espace supplémentaire et déplace les données existantes lorsque le bloc entrant ne tient pas. Comme l'a souligné Clifford, la mise en mémoire tampon dans une classe d'E / S de fichiers n'aurait pas à faire cela, elle viderait simplement la mémoire tampon actuelle et la réutiliser. Cela devrait donc être une limite supérieure du coût de la mise en mémoire tampon de la sortie. Et c'est exactement ce qu'il faut pour créer un tampon en mémoire qui fonctionne.
Alors, pourquoi est stringbuf
2,5 fois plus lent sur ideone, et au moins 10 fois plus lent lorsque je le teste? Il n'est pas utilisé de manière polymorphe dans ce micro-benchmark simple, donc cela ne l'explique pas.
la source
std::ostringstream
n'est pas assez intelligent pour augmenter de façon exponentielle sa taille de tampon comme c'est le casstd::vector
, c'est (A) stupide et (B) quelque chose que les gens qui pensent aux performances d'E / S devraient penser. Quoi qu'il en soit, le tampon est réutilisé, il n'est pas réaffecté à chaque fois. Etstd::vector
utilise également un tampon à croissance dynamique. J'essaie d'être juste ici.ostringstream
et que vous souhaitez des performances aussi rapides que possible, vous devriez envisager d'aller directement àstringbuf
. Lesostream
classes sont supposées lier la fonctionnalité de formatage sensible aux paramètres régionaux avec un choix de tampon flexible (fichier, chaîne, etc.) viardbuf()
et son interface de fonction virtuelle. Si vous ne faites aucun formatage, ce niveau supplémentaire d'indirection va certainement sembler proportionnellement cher par rapport à d'autres approches.ofstream
àfprintf
lors de la sortie d'informations de journalisation impliquant des doublons. MSVC 2008 sur WinXPsp3. iostreams est juste un chien lent.Réponses:
Ne répondant pas tant aux détails de votre question qu'au titre: le rapport technique 2006 sur les performances C ++ contient une section intéressante sur les flux IOS (p.68). Le plus pertinent pour votre question se trouve dans la section 6.1.2 («Vitesse d'exécution»):
Depuis la rédaction du rapport en 2006, on pourrait espérer que de nombreuses recommandations auraient été intégrées aux compilateurs actuels, mais ce n'est peut-être pas le cas.
Comme vous le mentionnez, les facettes peuvent ne pas figurer dans
write()
(mais je ne le suppose pas aveuglément). Alors, qu'est-ce qui caractérise? Exécuter GProf sur votreostringstream
code compilé avec GCC donne la répartition suivante: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)
Ainsi, la majeure partie du temps est consacrée à
xsputn
, ce qui finit par appelerstd::copy()
après de nombreuses vérifications et mises à jour des positions et des tampons du curseur (consultezc++\bits\streambuf.tcc
les détails).Je pense que vous vous êtes concentré sur le pire des cas. Toutes les vérifications effectuées ne représenteraient qu'une petite fraction du travail total effectué si vous aviez à traiter des blocs de données assez volumineux. Mais votre code déplace les données sur quatre octets à la fois et entraîne chaque fois tous les coûts supplémentaires. De toute évidence, on éviterait de le faire dans une situation réelle - considérez à quel point la pénalité aurait été négligeable si elle
write
avait été appelée sur un tableau de 1 m d'entiers au lieu de 1 m de fois sur un int. Et dans une situation réelle, on apprécierait vraiment les caractéristiques importantes d'IOStreams, à savoir sa conception à mémoire sûre et à type sûr. Ces avantages ont un prix, et vous avez écrit un test qui fait que ces coûts dominent le temps d'exécution.la source
ostream::write()
.sizeof i
, mais tous les compilateurs avec lesquels je teste ont 4 octetsint
). Et cela ne me semble pas du tout irréaliste, à quelle taille de morceaux pensez-vous passer à chaque appelxsputn
dans du code typique commestream << "VAR: " << var.x << ", " << var.y << endl;
.xsputn
cinq fois pourrait très bien se trouver dans une boucle qui écrit un fichier de 10 millions de lignes. Passer des données à des iostreams en gros morceaux est beaucoup moins un scénario réel que mon code de référence. Pourquoi devrais-je avoir à écrire dans un flux en mémoire tampon avec le nombre minimal d'appels? Si je dois faire ma propre mise en mémoire tampon, quel est l'intérêt des iostreams de toute façon? Et avec les données binaires, j'ai la possibilité de les tamponner moi-même, lorsque j'écris des millions de nombres dans un fichier texte, l'option en bloc n'existe tout simplement pas, je DOIS appeleroperator <<
pour chacun.Je suis plutôt déçu par les utilisateurs de Visual Studio, qui ont plutôt eu un aperçu de celui-ci:
ostream
, l'sentry
objet (qui est requis par la norme) entre dans une section critique protégeant lestreambuf
(qui n'est pas requis). Cela ne semble pas être facultatif, vous payez donc le coût de la synchronisation des threads même pour un flux local utilisé par un seul thread, qui n'a pas besoin de synchronisation.Cela nuit au code qui utilise
ostringstream
sévèrement le pour formater les messages. L'utilisationstringbuf
directe de évite l'utilisation desentry
, mais les opérateurs d'insertion formatés ne peuvent pas fonctionner directement surstreambuf
s. Pour Visual C ++ 2010, la section critique ralentitostringstream::write
d'un facteur trois par rapport à l'stringbuf::sputn
appel sous - jacent .En regardant les données du profileur de beldaz sur newlib , il semble clair que gcc
sentry
ne fait rien de fou comme ça.ostringstream::write
sous gcc ne prend environ 50% de plus questringbuf::sputn
, maisstringbuf
lui-même est beaucoup plus lent que sous VC ++. Et les deux se comparent toujours très défavorablement à l'utilisation d'unvector<char>
tampon d'E / S, mais pas avec la même marge que sous VC ++.la source
sentry
... "La sentinelle de classe définit une classe qui est responsable des opérations de préfixe et de suffixe sans exception." et une note "Le constructeur et le destructeur sentinelle peuvent également effectuer des opérations supplémentaires en fonction de l'implémentation." On peut également présumer du principe C ++ de «vous ne payez pas pour ce que vous n'utilisez pas» que le comité C ++ n'approuverait jamais une telle exigence de gaspillage. Mais n'hésitez pas à poser une question sur la sécurité des fils iostream.Le problème que vous voyez est tout dans la surcharge autour de chaque appel à write (). Chaque niveau d'abstraction que vous ajoutez (char [] -> vector -> string -> ostringstream) ajoute un peu plus d'appels de fonction / retours et d'autres guff de gestion qui - si vous l'appelez un million de fois - s'additionnent.
J'ai modifié deux des exemples sur ideone pour écrire dix pouces à la fois. Le temps en aval est passé de 53 à 6 ms (près de 10 fois l'amélioration) tandis que la boucle char s'est améliorée (3,7 à 1,5) - utile, mais seulement par un facteur de deux.
Si vous êtes préoccupé par les performances, vous devez choisir le bon outil pour le travail. ostringstream est utile et flexible, mais il y a une pénalité pour l'utiliser comme vous essayez. char [] est un travail plus difficile, mais les gains de performances peuvent être importants (rappelez-vous que le gcc inclura probablement les memcpys pour vous également).
En bref, ostringstream n'est pas cassé, mais plus vous vous rapprochez du métal, plus votre code s'exécutera rapidement. L'assembleur a encore des avantages pour certains.
la source
ostringstream::write()
doit faire çavector::push_back()
non? Si quoi que ce soit, cela devrait être plus rapide car il est remis un bloc au lieu de quatre éléments individuels. Siostringstream
c'est plus lent questd::vector
sans fournir de fonctionnalités supplémentaires, alors je dirais que c'est cassé.stringbuf
directe ne supprimera pas tous les appels de fonction carstringbuf
l'interface publique de se compose de fonctions publiques non virtuelles dans la classe de base qui sont ensuite envoyées à la fonction virtuelle protégée dans la classe dérivée.sputn
fonction publique qui appelle le virtuel protégéxsputn
, soit en ligne. Même s'ilxsputn
n'est pas en ligne, le compilateur peut, tout en étant en lignesputn
, déterminer lexsputn
remplacement exact nécessaire et générer un appel direct sans passer par la vtable.Pour obtenir de meilleures performances, vous devez comprendre comment fonctionnent les conteneurs que vous utilisez. Dans votre exemple de tableau char [], le tableau de la taille requise est alloué à l'avance. Dans votre exemple vectoriel et ostringstream, vous forcez les objets à allouer et réallouer à plusieurs reprises et éventuellement à copier des données plusieurs fois à mesure que l'objet se développe.
Avec std :: vector, cela est facilement résolu en initialisant la taille du vecteur à la taille finale comme vous l'avez fait pour le tableau char; au lieu de cela, vous paralysez injustement les performances en redimensionnant à zéro! Ce n'est pas une comparaison juste.
En ce qui concerne l'ostringstream, la préallocation de l'espace n'est pas possible, je dirais que c'est une utilisation inappropriée. La classe a beaucoup plus d'utilité qu'un simple tableau de caractères, mais si vous n'avez pas besoin de cet utilitaire, ne l'utilisez pas, car vous paierez les frais généraux dans tous les cas. Au lieu de cela, il doit être utilisé pour ce qu'il est bon - le formatage des données dans une chaîne. C ++ fournit une large gamme de conteneurs et un ostringstram est parmi les moins appropriés à cet effet.
Dans le cas du vecteur et de l'ostringstream, vous obtenez une protection contre le dépassement de tampon, vous n'obtenez pas cela avec un tableau de caractères, et cette protection n'est pas gratuite.
la source
ostringstream.str.reserve(4000000)
et cela n'a fait aucune différence.ostringstream
, vous pouvez "réserver" en passant une chaîne fictive, c'est-à-dire:ostringstream str(string(1000000 * sizeof(int), '\0'));
avecvector
, leresize
ne désalloue aucun espace, il ne se dilate que s'il le faut.vector[]
opérateur n'est généralement PAS vérifié pour les erreurs de limites par défaut.vector.at()
est cependant.vector<T>::resize(0)
neoperator[]
, maispush_back()
(à titre deback_inserter
), ce qui teste définitivement le débordement. Ajout d'une autre version qui n'utilise paspush_back
.