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.
fuente
std::ostringstream
no es lo suficientemente inteligente como para aumentar exponencialmente su tamaño de búfer como lostd::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. Ystd::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 astringbuf
. Seostream
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 derdbuf()
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
afprintf
cuando se genera información de registro que involucra dobles. MSVC 2008 en WinXPsp3. iostreams es simplemente perro lento.Respuestas:
No responde tanto a los detalles de su pregunta como al título: el Informe técnico 2006 sobre el rendimiento de C ++ tiene una sección interesante sobre IOStreams (p.68). Lo más relevante para su pregunta se encuentra en la Sección 6.1.2 ("Velocidad de ejecución"):
Dado que el informe fue escrito en 2006, uno esperaría que muchas de las recomendaciones se hubieran incorporado a los compiladores actuales, pero quizás este no sea el caso.
Como mencionas, las facetas pueden no aparecer
write()
(pero no asumiría eso a ciegas). Entonces, ¿qué presenta? Ejecutar GProf en suostringstream
código compilado con GCC proporciona el siguiente desglose: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)
Por lo tanto, se dedica la mayor parte del tiempo
xsputn
, lo que finalmente requierestd::copy()
después de muchas comprobaciones y actualizaciones de las posiciones del cursor y los búferes (eche un vistazoc++\bits\streambuf.tcc
a los detalles).Mi opinión sobre esto es que te has centrado en la peor situación. Toda la verificación que se realice sería una pequeña fracción del trabajo total realizado si se tratara de fragmentos de datos razonablemente grandes. Pero su código está cambiando datos en cuatro bytes a la vez e incurriendo en todos los costos adicionales cada vez. Claramente, uno evitaría hacerlo en una situación de la vida real: considere cuán insignificante hubiera sido la penalización si
write
se hubiera llamado en una matriz de 1 millón de entradas en lugar de 1 millón de veces en un int. Y en una situación de la vida real, uno realmente apreciaría las características importantes de IOStreams, a saber, su diseño seguro para la memoria y el tipo. Tales beneficios tienen un precio, y usted ha escrito una prueba que hace que estos costos dominen el tiempo de ejecución.fuente
ostream::write()
.sizeof i
, pero todos los compiladores con los que estoy probando tienen 4 bytesint
). Y eso no me parece tan poco realista, ¿a qué tamaño crees que se pasa en cada llamadaxsputn
en un código típicostream << "VAR: " << var.x << ", " << var.y << endl;
?xsputn
cinco veces podría muy bien estar dentro de un bucle que escribe un archivo de 10 millones de líneas. Pasar datos a iostreams en grandes fragmentos es mucho menos un escenario de la vida real que mi código de referencia. ¿Por qué debería tener que escribir en una secuencia almacenada con el número mínimo de llamadas? Si tengo que hacer mi propio almacenamiento en búfer, ¿cuál es el punto de los iostreams de todos modos? Y con los datos binarios, tengo la opción de almacenarlo en el búfer, cuando escribo millones de números en un archivo de texto, la opción masiva simplemente no existe, DEBO llamaroperator <<
a cada uno.Estoy bastante decepcionado con los usuarios de Visual Studio, que prefieren darme una idea de esto:
ostream
, elsentry
objeto (que es requerido por el estándar) entra en una sección crítica que protege elstreambuf
(que no es requerido). Esto no parece ser opcional, por lo que paga el costo de la sincronización de subprocesos incluso para una secuencia local utilizada por un solo subproceso, que no tiene necesidad de sincronización.Esto perjudica el código que se usa
ostringstream
para formatear mensajes con bastante severidad. Usarstringbuf
directamente evita el uso desentry
, pero los operadores de inserción formateados no pueden funcionar directamente enstreambuf
s. Para Visual C ++ 2010, la sección crítica se ralentiza enostringstream::write
un factor de tres frente a lastringbuf::sputn
llamada subyacente .Mirando los datos del perfil de beldaz en newlib , parece claro que los gcc
sentry
no hacen nada loco como este.ostringstream::write
bajo gcc solo toma aproximadamente un 50% más de tiempostringbuf::sputn
, pero enstringbuf
sí mismo es mucho más lento que bajo VC ++. Y ambos todavía se comparan muy desfavorablemente con el uso de unvector<char>
búfer de E / S, aunque no por el mismo margen que en VC ++.fuente
sentry
... "El centinela de clase define una clase que es responsable de realizar operaciones de sufijo y sufijo de excepción segura". y una nota "El constructor y destructor centinela también pueden realizar operaciones adicionales dependientes de la implementación". También se puede deducir del principio de C ++ de "no paga por lo que no usa" que el comité de C ++ nunca aprobaría un requisito tan inútil. Pero siéntase libre de hacer una pregunta sobre la seguridad del hilo iostream.El problema que ves está en los gastos generales alrededor de cada llamada a write (). Cada nivel de abstracción que agregue (char [] -> vector -> string -> ostringstream) agrega algunas llamadas / devoluciones de funciones más y otras características de limpieza que, si lo llama un millón de veces, se suman.
Modifiqué dos de los ejemplos de ideone para escribir diez entradas a la vez. El tiempo de ostringstream pasó de 53 a 6 ms (casi 10 veces mejoría) mientras que el ciclo de char mejoró (3.7 a 1.5), útil, pero solo por un factor de dos.
Si le preocupa tanto el rendimiento, debe elegir la herramienta adecuada para el trabajo. ostringstream es útil y flexible, pero hay una penalización por usarlo de la manera que lo intentas. char [] es un trabajo más duro, pero las ganancias de rendimiento pueden ser excelentes (recuerde que el gcc probablemente también incluirá los memcpys para usted).
En resumen, ostringstream no está roto, pero cuanto más te acerques al metal, más rápido se ejecutará tu código. Assembler todavía tiene ventajas para algunas personas.
fuente
ostringstream::write()
tiene que hacer quevector::push_back()
no? En todo caso, debería ser más rápido ya que se entrega un bloque en lugar de cuatro elementos individuales. Siostringstream
es más lento questd::vector
sin proporcionar ninguna característica adicional, entonces sí lo llamaría roto.stringbuf
directo no eliminará todas las llamadas a funciones, ya questringbuf
la interfaz pública consiste en funciones públicas no virtuales en la clase base que luego se envían a la función virtual protegida en la clase derivada.sputn
función pública que llama a lo virtual protegidoxsputn
, esté en línea. Incluso sixsputn
no está en línea, el compilador puede, mientras está en líneasputn
, determinar laxsputn
anulación exacta necesaria y generar una llamada directa sin pasar por la tabla.Para obtener un mejor rendimiento, debe comprender cómo funcionan los contenedores que está utilizando. En su ejemplo de matriz char [], la matriz del tamaño requerido se asigna por adelantado. En su ejemplo de vector y ostringstream, está obligando a los objetos a asignar y reasignar repetidamente y posiblemente copiar datos muchas veces a medida que el objeto crece.
Con std :: vector, esto se resuelve fácilmente inicializando el tamaño del vector al tamaño final como lo hizo con la matriz de caracteres; ¡en lugar de eso, paraliza injustamente el rendimiento cambiando el tamaño a cero! Esa no es una comparación justa.
Con respecto al ostringstream, no es posible preasignar el espacio, sugeriría que es un uso inapropiado. La clase tiene una utilidad mucho mayor que una simple matriz de caracteres, pero si no necesita esa utilidad, no la use, porque en cualquier caso pagará los gastos generales. En su lugar, debe usarse para lo que es bueno: formatear datos en una cadena. C ++ proporciona una amplia gama de contenedores y un ostringstram es uno de los menos apropiados para este propósito.
En el caso del vector y el ostringstream, obtienes protección contra el desbordamiento del búfer, no lo obtienes con una matriz de caracteres, y esa protección no es gratuita.
fuente
ostringstream.str.reserve(4000000)
y no hizo ninguna diferencia.ostringstream
, podría "reservar" pasando una cadena ficticia, es decir:ostringstream str(string(1000000 * sizeof(int), '\0'));
convector
,resize
no desasigna ningún espacio, solo se expande si es necesario.vector[]
operador generalmente NO se verifica por errores de límites de forma predeterminada.vector.at()
es sin embargo.vector<T>::resize(0)
no suele reasignar la memoriaoperator[]
, peropush_back()
(a modo deback_inserter
), lo que definitivamente prueba el desbordamiento. Se agregó otra versión que no usapush_back
.