Cada vez que menciono el lento rendimiento de los iostreams de la biblioteca estándar de C ++, me encuentro con una ola de incredulidad. Sin embargo, tengo resultados del generador de perfiles que muestran grandes cantidades de tiempo invertido en el código de la biblioteca iostream (optimizaciones completas del compilador), y el cambio de iostreams a API de E / S específicas del sistema operativo y la gestión personalizada del búfer da una mejora en el orden de magnitud.
¿Qué trabajo adicional está haciendo la biblioteca estándar de C ++, es requerido por el estándar y es útil en la práctica? ¿O algunos compiladores proporcionan implementaciones de iostreams que son competitivas con la gestión manual del búfer?
Puntos de referencia
Para que las cosas se muevan, he escrito un par de programas cortos para ejercer el búfer interno iostreams:
- poner datos binarios en un
ostringstream
http://ideone.com/2PPYw - poner datos binarios en un
char[]
búfer http://ideone.com/Ni5ct - poner datos binarios en un http://ideone.com/Mj2Fi
vector<char>
usandoback_inserter
- NUEVO :
vector<char>
iterador simple http://ideone.com/9iitv - NUEVO : poner datos binarios directamente en
stringbuf
http://ideone.com/qc9QA - NUEVO :
vector<char>
iterador simple más verificación de límites http://ideone.com/YyrKy
Tenga en cuenta que las versiones ostringstream
y stringbuf
ejecutan menos iteraciones porque son mucho más lentas.
En ideone, ostringstream
es aproximadamente 3 veces más lento que std:copy
+ back_inserter
+ std::vector
, y aproximadamente 15 veces más lento que memcpy
en un búfer sin procesar. Esto se siente consistente con el perfil de antes y después cuando cambié mi aplicación real al almacenamiento en búfer personalizado.
Todos estos son buffers en memoria, por lo que la lentitud de los iostreams no se puede atribuir a la E / S de disco lento, demasiado enjuague, sincronización con stdio o cualquiera de las otras cosas que las personas usan para excusar la lentitud observada de la biblioteca estándar de C ++ iostream
Sería bueno ver puntos de referencia en otros sistemas y comentarios sobre las cosas que hacen las implementaciones comunes (como libc ++ de gcc, Visual C ++, Intel C ++) y qué cantidad de sobrecarga es requerida por el estándar.
Justificación de esta prueba
Varias personas han señalado correctamente que los iostreams se usan más comúnmente para la salida formateada. Sin embargo, también son la única API moderna proporcionada por el estándar C ++ para el acceso a archivos binarios. Pero la verdadera razón para realizar pruebas de rendimiento en el almacenamiento en búfer interno se aplica a la E / S formateada típica: si iostreams no puede mantener el controlador de disco suministrado con datos sin procesar, ¿cómo pueden mantener el ritmo cuando también son responsables del formateo?
Tiempo de referencia
Todos estos son por iteración del k
bucle externo ( ).
En ideone (gcc-4.3.4, sistema operativo y hardware desconocidos):
ostringstream
: 53 milisegundosstringbuf
: 27 msvector<char>
yback_inserter
: 17,6 msvector<char>
con iterador ordinario: 10,6 msvector<char>
iterador y verificación de límites: 11.4 mschar[]
: 3,7 ms
En mi computadora portátil (Visual C ++ 2010 x86`` cl /Ox /EHsc
Windows 7 Ultimate 64-bit, Intel Core i7, 8 GB RAM):
ostringstream
: 73,4 milisegundos, 71,6 msstringbuf
: 21,7 ms, 21,3 msvector<char>
yback_inserter
: 34,6 ms, 34,4 msvector<char>
con iterador ordinario: 1.10 ms, 1.04 msvector<char>
iterador y verificación de límites: 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, con perfil guiada por la optimización cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, carrera, link /ltcg:pgo
, medida:
ostringstream
: 61,2 ms, 60,5 msvector<char>
con iterador ordinario: 1.04 ms, 1.03 ms
La misma computadora portátil, el mismo sistema operativo, usando cygwin gcc 4.3.4 g++ -O3
:
ostringstream
: 62,7 ms, 60,5 msstringbuf
: 44,4 ms, 44,5 msvector<char>
yback_inserter
: 13,5 ms, 13,6 msvector<char>
con iterador ordinario: 4.1 ms, 3.9 msvector<char>
iterador y verificación de límites: 4.0 ms, 4.0 mschar[]
: 3,57 ms, 3,75 ms
Mismo equipo portátil, Visual C ++ 2008 SP1, cl /Ox /EHsc
:
ostringstream
: 88,7 ms, 87,6 msstringbuf
: 23,3 ms, 23,4 msvector<char>
yback_inserter
: 26,1 ms, 24,5 msvector<char>
con iterador ordinario: 3.13 ms, 2.48 msvector<char>
iterador y verificación de límites: 2.97 ms, 2.53 mschar[]
: 1,52 ms, 1,25 ms
Mismo portátil, compilador de Visual C ++ 2010 de 64 bits:
ostringstream
: 48,6 ms, 45,0 msstringbuf
: 16,2 ms, 16,0 msvector<char>
yback_inserter
: 26,3 ms, 26,5 msvector<char>
con iterador ordinario: 0,87 ms, 0,89 msvector<char>
iterador y verificación de límites: 0,99 ms, 0,99 mschar[]
: 1,25 ms, 1,24 ms
EDITAR: corrió todo dos veces para ver qué tan consistentes fueron los resultados. OMI bastante consistente.
NOTA: en mi computadora portátil, dado que puedo ahorrar más tiempo de CPU de lo que permite ideone, configuro el número de iteraciones en 1000 para todos los métodos. Esto significa que ostringstream
y la vector
reasignación, que se realiza solo en el primer pase, debería tener poco impacto en los resultados finales.
EDITAR: Vaya, se encontró un error en el vector
iterador -with-ordinario, el iterador no se estaba avanzando y, por lo tanto, había demasiados hits de caché. Me preguntaba cómo vector<char>
estaba superando char[]
. Sin embargo, no hizo mucha diferencia, vector<char>
todavía es más rápido que char[]
con VC ++ 2010.
Conclusiones
El almacenamiento en búfer de las secuencias de salida requiere tres pasos cada vez que se agregan datos:
- Compruebe que el bloque entrante se ajusta al espacio de búfer disponible.
- Copia el bloque entrante.
- Actualice el puntero de fin de datos.
El último fragmento de código que publiqué, " vector<char>
iterador simple más verificación de límites" no solo hace esto, sino que también asigna espacio adicional y mueve los datos existentes cuando el bloque entrante no encaja. Como señaló Clifford, el almacenamiento en búfer en una clase de E / S de archivo no tendría que hacer eso, simplemente vaciaría el búfer actual y lo reutilizaría. Por lo tanto, esto debería ser un límite superior en el costo de la producción de almacenamiento en búfer. Y es exactamente lo que se necesita para hacer un búfer en memoria que funcione.
Entonces, ¿por qué es stringbuf
2.5 veces más lento en ideone, y al menos 10 veces más lento cuando lo pruebo? No se está utilizando polimórficamente en este simple micro-punto de referencia, por lo que eso no lo explica.
std::ostringstream
no es lo suficientemente inteligente como para aumentar exponencialmente su tamaño de búfer como lo std::vector
hace, eso es (A) estúpido y (B) algo en lo que las personas que piensan sobre el rendimiento de E / S deberían pensar. De todos modos, el búfer se reutiliza, no se reasigna cada vez. Y std::vector
también está utilizando un búfer de crecimiento dinámico. Estoy tratando de ser justo aquí.
ostringstream
y desea un rendimiento lo más rápido posible, entonces debería considerar ir directamente a stringbuf
. Se ostream
supone que las clases unen la funcionalidad de formato compatible con la configuración regional con la opción de almacenamiento intermedio flexible (archivo, cadena, etc.) a través de rdbuf()
su interfaz de función virtual. Si no está formateando, entonces ese nivel adicional de indirección ciertamente se verá proporcionalmente caro en comparación con otros enfoques.
ofstream
a fprintf
cuando se genera información de registro que involucra dobles. MSVC 2008 en WinXPsp3. iostreams es simplemente perro lento.