programa - librerias de c++
¿El estándar de C++ exige un bajo rendimiento para iostreams, o simplemente estoy tratando con una implementación deficiente? (4)
El problema que ves está en la parte superior de cada llamada para escribir (). Cada nivel de abstracción que agregue (char [] -> vector -> string -> ostringstream) agrega algunas llamadas / devoluciones de funciones más y otros guiños de limpieza que, si lo llama un millón de veces, se acumula.
Modifiqué dos de los ejemplos en ideone para escribir diez entradas a la vez. El tiempo del ostringstream pasó de 53 a 6 ms (casi 10 veces la mejora) mientras que el ciclo de funcionamiento mejorado (de 3,7 a 1,5) fue útil, pero solo en un factor de dos.
Si le preocupa el rendimiento, debe elegir la herramienta adecuada para el trabajo. ostringstream es útil y flexible, pero hay una penalización por usarlo de la forma en que lo intentas. char [] es un trabajo más difícil, pero los aumentos de rendimiento pueden ser excelentes (recuerde que el gcc probablemente alineará los memcpys también para usted).
En resumen, ostringstream no está roto, pero cuanto más se acerque al metal, más rápido se ejecutará tu código. El ensamblador todavía tiene ventajas para algunas personas.
Cada vez que menciono el bajo rendimiento de los iostreams de la biblioteca estándar de C ++, me encuentro con una ola de incredulidad. Sin embargo, tengo resultados de perfil que muestran grandes cantidades de tiempo en el código de la biblioteca iostream (optimizaciones completas del compilador) y el cambio de iostreams a las API de E / S específicas del sistema operativo y la administración de búfer personalizada da una mejora de orden de magnitud.
¿Qué trabajo adicional está haciendo la biblioteca estándar de C ++, es requerido por la norma y es útil en la práctica? ¿O algunos compiladores proporcionan implementaciones de iostreams que son competitivos con la administración manual de buffer?
Puntos de referencia
Para que todo funcione, he escrito un par de breves programas para ejercitar el almacenamiento intermedio interno de iostreams:
- poner datos binarios en un
ostringstream
http://ideone.com/2PPYw - poner datos binarios en un búfer
char[]
http://ideone.com/Ni5ct - poner datos binarios en un
vector<char>
usandoback_inserter
http://ideone.com/Mj2Fi - NUEVO :
vector<char>
simple iterator http://ideone.com/9iitv - NUEVO : poner datos binarios directamente en
stringbuf
http://ideone.com/qc9QA - NUEVO :
vector<char>
simple iterator plus bounds check http://ideone.com/YyrKy
Tenga en cuenta que las versiones ostringstream
y stringbuf
ejecutan menos iteraciones porque son mucho más lentas.
En ideone, el ostringstream
es aproximadamente 3 veces más lento que std:copy
+ back_inserter
+ std::vector
, y unas 15 veces más lento que memcpy
en un buffer en bruto. Esto se siente consistente con el perfil de antes y después cuando cambié mi aplicación real al almacenamiento en búfer personalizado.
Estos son todos los búferes en memoria, por lo que la lentitud de iostreams no se puede atribuir a la E / S de disco lento, el enrojecimiento excesivo, la sincronización con stdio o cualquiera de las otras cosas que la gente usa para justificar la lentitud observada de la biblioteca estándar de C ++ iostream.
Sería bueno ver puntos de referencia en otros sistemas y comentarios sobre cosas que hacen las implementaciones comunes (como gcc''s libc ++, Visual C ++, Intel C ++) y la cantidad de sobrecarga que exige 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 acceso a archivos binarios. Pero el verdadero motivo para realizar pruebas de rendimiento en el búfer interno se aplica a la E / S formateada típica: si iostreams no pueden mantener el controlador de disco con datos sin procesar, ¿cómo pueden mantenerse al día cuando también son responsables del formateo?
Tiempo de referencia
Todos estos son por iteración del bucle externo ( k
).
En ideone (gcc-4.3.4, sistema operativo y hardware desconocidos):
-
ostringstream
: 53 milisegundos -
stringbuf
: 27 ms -
vector<char>
yback_inserter
: 17.6 ms -
vector<char>
con iterador ordinario: 10.6 ms - Comprobación del iterador y los límites del
vector<char>
: 11.4 ms -
char[]
: 3.7 ms
En mi computadora portátil (Visual C ++ 2010 x86, cl /Ox /EHsc
, Windows 7 Ultimate de 64 bits, Intel Core i7, 8 GB de RAM):
-
ostringstream
: 73,4 milisegundos, 71,6 ms -
stringbuf
: 21.7 ms, 21.3 ms -
vector<char>
yback_inserter
: 34.6 ms, 34.4 ms -
vector<char>
con iterador ordinario: 1.10 ms, 1.04 ms - Comprobación del iterador y los límites del
vector<char>
: 1.11 ms, 0.87 ms, 1.12 ms, 0.89 ms, 1.02 ms, 1.14 ms -
char[]
: 1.48 ms, 1.57 ms
Visual C ++ 2010 x86, con Optimización guiada por perfil cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, ejecutar, link /ltcg:pgo
, medida:
-
ostringstream
: 61.2 ms, 60.5 ms -
vector<char>
con iterador ordinario: 1.04 ms, 1.03 ms
La misma computadora portátil, el mismo sistema operativo, que usa cygwin gcc 4.3.4 g++ -O3
:
-
ostringstream
: 62.7 ms, 60.5 ms -
stringbuf
: 44.4 ms, 44.5 ms -
vector<char>
yback_inserter
: 13.5 ms, 13.6 ms -
vector<char>
con iterador ordinario: 4.1 ms, 3.9 ms - Verificador de
vector<char>
y verificación de límites: 4.0 ms, 4.0 ms -
char[]
: 3.57 ms, 3.75 ms
La misma computadora portátil, Visual C ++ 2008 SP1, cl /Ox /EHsc
:
-
ostringstream
: 88.7 ms, 87.6 ms -
stringbuf
: 23.3 ms, 23.4 ms -
vector<char>
yback_inserter
: 26.1 ms, 24.5 ms -
vector<char>
con iterador ordinario: 3.13 ms, 2.48 ms - Comprobación del iterador y los límites del
vector<char>
: 2.97 ms, 2.53 ms -
char[]
: 1.52 ms, 1.25 ms
Mismo equipo portátil, compilador de Visual C ++ 2010 de 64 bits:
-
ostringstream
: 48.6 ms, 45.0 ms -
stringbuf
: 16.2 ms, 16.0 ms -
vector<char>
yback_inserter
: 26.3 ms, 26.5 ms -
vector<char>
con iterador ordinario: 0.87 ms, 0.89 ms - Comprobación del iterador y los límites del
vector<char>
: 0,99 ms, 0,99 ms -
char[]
: 1.25 ms, 1.24 ms
EDITAR: ejecutó todo dos veces para ver qué tan consistentes eran los resultados. IMO bastante consistente.
NOTA: en mi computadora portátil, dado que puedo ahorrar más tiempo de CPU de lo que permite ideone, establezco el número de iteraciones en 1000 para todos los métodos. Esto significa que la reasignación de ostringstream
y vector
ostringstream
, que tiene lugar solo en la primera pasada, debería tener poco impacto en los resultados finales.
EDITAR: Vaya, encontré un error en el vector
-with-ordinary-iterator, el iterador no se estaba avanzando y, por lo tanto, había demasiados toques de caché. Me preguntaba cómo vector<char>
estaba superando a char[]
. Sin embargo, no hizo mucha diferencia, el vector<char>
es aún más rápido que char[]
en VC ++ 2010.
Conclusiones
El almacenamiento en búfer de flujos de salida requiere tres pasos cada vez que se añaden datos:
- Verifique que el bloque entrante se ajuste 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>
simple iterator plus bounds check" 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 archivos no tendría que hacer eso, simplemente eliminaría el búfer en uso y lo reutilizaría. Así que esto debería ser un límite superior en el costo de la salida de almacenamiento en búfer. Y es exactamente lo que se necesita para hacer un buffer en memoria que funcione.
Entonces, ¿por qué es stringbuf
2.5x más lento en ideone, y al menos 10 veces más lento cuando lo pruebo? No se usa de forma polimórfica en este micro-benchmark simple, por lo que no lo explica.
Estoy bastante decepcionado con los usuarios de Visual Studio, que más bien tuvieron un truco con este:
- En la implementación de Visual Studio de
ostream
, el objetosentry
(requerido por el estándar) ingresa en una sección crítica que protege elstreambuf
(que no es obligatorio). Esto no parece ser opcional, por lo que paga el costo de sincronización de subprocesos incluso para una secuencia local utilizada por un solo subproceso, que no necesita sincronización.
Esto lastima el código que usa ostringstream
para formatear los mensajes bastante severamente. El uso de stringbuf
evita directamente el uso de sentry
, pero los operadores de inserción formateados no pueden trabajar directamente en streambuf
s. Para Visual C ++ 2010, la sección crítica está ralentizando ostringstream::write
por un factor de tres frente a la stringbuf::sputn
subyacente stringbuf::sputn
.
Al mirar los datos de perfil de beldaz en newlib , parece claro que el sentry
de gcc no hace nada loco como este. ostringstream::write
en gcc solo tarda aproximadamente un 50% más que stringbuf::sputn
, pero el stringbuf
sí mismo es mucho más lento que en VC ++. Y ambos aún se pueden comparar muy desfavorablemente al uso de un vector<char>
para el almacenamiento en búfer de E / S, aunque no por el mismo margen que en VC ++.
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 es en la Sección 6.1.2 ("Velocidad de ejecución"):
Dado que ciertos aspectos del procesamiento de IOStreams se distribuyen en múltiples facetas, parece que el Estándar exige una implementación ineficiente. Pero este no es el caso: al usar alguna forma de preprocesamiento, se puede evitar gran parte del trabajo. Con un engarce un poco más inteligente que el utilizado normalmente, es posible eliminar algunas de estas ineficiencias. Esto se discute en §6.2.3 y §6.2.5.
Dado que el informe se escribió en 2006, uno esperaría que muchas de las recomendaciones se hubieran incorporado a los compiladores actuales, pero tal vez este no sea el caso.
Como mencionas, las facetas pueden no aparecer en write()
(pero no lo asumiría a ciegas). Entonces, ¿qué función? La ejecución de GProf en su código ostringstream
compilado con GCC proporciona el siguiente desglose:
- 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% en
std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
- 0.96% en
std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
- 0.00% en
std::fpos<int>::fpos(long long)
Por lo tanto, la mayor parte del tiempo se gasta en xsputn
, que eventualmente llama a std::copy()
después de mucha comprobación y actualización de posiciones de cursor y búfers (eche un vistazo a c++/bits/streambuf.tcc
para más detalles).
Mi opinión sobre esto es que te has centrado en la peor situación posible. Toda la verificación que se realiza 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 habría sido la penalización si se hubiera invocado la write
en una matriz de 1m en lugar de en 1 millón de veces en una int. Y en una situación de la vida real, uno realmente apreciaría las características importantes de IOStreams, es decir, su diseño seguro y seguro para tipos de memoria. Dichos beneficios tienen un precio, y usted ha escrito una prueba que hace que estos costos dominen el tiempo de ejecución.
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 vectorial y ostringstream, está obligando a los objetos a asignar, reasignar 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 hiciste con la matriz de caracteres; ¡En cambio, usted injustamente paraliza el rendimiento al cambiar el tamaño a cero! Esa no es una comparación justa.
Con respecto a ostringstream, la preasignación del espacio no es posible, sugeriría que es un uso inapropiado. La clase tiene una utilidad mucho mayor que una simple matriz de caracteres, pero si no la necesita, no la utilice, ya que pagará los gastos generales en cualquier caso. En su lugar, debería usarse para lo que es bueno: formatear datos en una cadena. C ++ ofrece una amplia gama de contenedores y un ostringstram es uno de los menos apropiados para este propósito.
En el caso del vector y ostringstream, obtienes protección contra el desbordamiento del búfer, no obtienes eso con una matriz de caracteres, y esa protección no se obtiene de forma gratuita.