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.
std::ostringstream
n'est pas assez intelligent pour augmenter de façon exponentielle sa taille de tampon comme c'est le cas std::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. Et std::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
. Les ostream
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.) via rdbuf()
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.