sirven - Métricas cuantificables(puntos de referencia) sobre el uso de librerías de C++ con solo encabezado
librerias de c++ y sus funciones y para que sirven (3)
Intenté encontrar una respuesta a esto usando SO. Hay una serie de preguntas que enumeran los diversos pros y contras de construir una biblioteca de solo encabezado en c ++, pero no he podido encontrar uno que lo haga en términos cuantificables.
Entonces, en términos cuantificables, ¿qué diferencia hay entre usar el encabezado c ++ tradicionalmente separado y los archivos de implementación solo frente al encabezado?
Para simplificar, asumo que las plantillas no se utilizan (porque solo requieren encabezado).
Para elaborar, he enumerado lo que he visto en los artículos para ser los pros y los contras. Obviamente, algunos no son fácilmente cuantificables (como la facilidad de uso) y, por lo tanto, son inútiles para la comparación cuantificable. Marcaré aquellos que espero métricas cuantificables con un (cuantificable).
Pros para solo encabezado
- Es más fácil de incluir, ya que no necesita especificar opciones de vinculador en su sistema de compilación.
- Siempre compila todo el código de la biblioteca con el mismo compilador (opciones) que el resto de su código, ya que las funciones de la biblioteca se insertan en su código.
- Puede ser mucho más rápido. (cuantificable)
- Puede dar al compilador / enlazador mejores oportunidades para la optimización (explicación / cuantificable, si es posible)
- Es obligatorio si usa plantillas de todos modos.
Contras para solo encabezado
- Se hincha el código. (cuantificable) (¿cómo afecta eso el tiempo de ejecución y la huella de memoria)
- Tiempos de compilación más largos. (cuantificable)
- Pérdida de separación de interfaz e implementación.
- A veces conduce a dependencias circulares difíciles de resolver.
- Evita la compatibilidad binaria de bibliotecas compartidas / DLL.
- Puede agravar a los compañeros de trabajo que prefieren las formas tradicionales de usar C ++.
Cualquier ejemplo que pueda usar de proyectos de código abierto más grandes (comparando bases de código de tamaño similar) sería muy apreciado. O bien, si conoce un proyecto que puede cambiar entre versiones solo encabezadas y separadas (utilizando un tercer archivo que incluya ambas), sería ideal. Los números anecdóticos también son útiles porque me dan un estadio con el que puedo obtener una idea.
fuentes de pros y contras:
- https://stackoverflow.com/a/6200793/278976
- https://stackoverflow.com/a/1783905/278976
Gracias por adelantado...
ACTUALIZAR:
Para cualquiera que pueda estar leyendo esto más tarde y esté interesado en obtener un poco de información básica sobre cómo enlazar y compilar, encontré que estos recursos son útiles:
- Capítulo 7 de http://www.amazon.com/Computer-Systems-Programmers-Perspective-Edition/dp/0136108040
- http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html
- http://www.cyberciti.biz/tips/linux-shared-library-management.html
ACTUALIZACIÓN: (en respuesta a los comentarios a continuación)
El hecho de que las respuestas puedan variar no significa que la medición sea inútil. Tienes que comenzar a medir como un punto. Y cuantas más mediciones tenga, más clara será la imagen. Lo que estoy pidiendo en esta pregunta no es toda la historia, sino un vistazo de la imagen. Claro, cualquiera puede usar números para sesgar un argumento si quieren promover de manera poco ética su parcialidad. Sin embargo, si alguien siente curiosidad acerca de las diferencias entre dos opciones y publica esos resultados, creo que la información es útil.
¿Nadie ha tenido curiosidad sobre este tema, lo suficiente como para medirlo?
Me encanta el proyecto de tiroteo. Podríamos comenzar eliminando la mayoría de esas variables. Solo use una versión de gcc en una versión de Linux. Solo use el mismo hardware para todos los puntos de referencia. No compilar con múltiples hilos.
Entonces, podemos medir:
- tamaño ejecutable
- tiempo de ejecución
- huella de memoria
- tiempo de compilación (para todo el proyecto y cambiando un archivo)
- tiempo de enlace
Espero que esto no sea muy similar a lo que dijo Realz.
Tamaño del ejecutable (/ objeto): (ejecutable 0% / objeto hasta 50% más grande solo en el encabezado)
Asumiría que las funciones definidas en un archivo de encabezado se copiarán en cada objeto. Cuando se trata de generar el ejecutable, diría que debería ser bastante fácil cortar las funciones duplicadas (no tengo idea de qué enlazadores hacen / no hacen esto, supongo que la mayoría lo hace), así que (probablemente) no hay diferencia real en el tamaño ejecutable, pero bien en el tamaño del objeto. La diferencia debería depender en gran medida de la cantidad de código realmente en los encabezados versus el resto del proyecto. No es que el tamaño del objeto realmente importe en estos días, a excepción del tiempo de enlace.
Tiempo de ejecución: (1%)
Yo diría básicamente idéntico (una dirección de función es una dirección de función), excepto para las funciones en línea. Esperaría que las funciones en línea tuvieran menos de un 1% de diferencia en tu programa promedio, porque las llamadas a funciones tienen cierta sobrecarga, pero esto no es nada comparado con la sobrecarga de hacer algo con un programa.
Huella de memoria: (0%)
Lo mismo en el ejecutable = la misma huella de memoria (durante el tiempo de ejecución), suponiendo que el enlazador corta las funciones duplicadas. Si no se recortan las funciones duplicadas, puede hacer una gran diferencia.
Tiempo de compilación (para todo el proyecto y cambiando un archivo): (todo hasta 50% más rápido para uno, solo hasta 99% más rápido para no encabezado solamente)
Gran diferencia. Al cambiar algo en el archivo de encabezado, todo lo que lo incluye se recompila, mientras que los cambios en un archivo cpp solo requieren que el objeto se vuelva a crear y se vuelva a vincular. Y un fácil 50% más lento para una compilación completa para las bibliotecas de solo encabezado. Sin embargo, con compilaciones precompiladas o compilaciones unitarias, una compilación completa con librerías solo de encabezado probablemente sería más rápida, pero un cambio que requiere una gran cantidad de archivos para recompilar es una gran desventaja, y yo diría que eso no lo vale. . Las recompilaciones completas no son necesarias a menudo. Además, puede incluir algo en un archivo cpp pero no en su archivo de cabecera (esto puede suceder a menudo), por lo tanto, en un programa diseñado adecuadamente (estructura / dependencia de dependencia similar a un árbol), al cambiar una declaración de función o algo (siempre requiere cambios en el archivo de encabezado), solo encabezado provocaría muchas cosas para recompilar, pero con no encabezado solo puede limitar esto en gran medida.
Tiempo de enlace: (hasta 50% más rápido para solo encabezado)
Los objetos son probablemente más grandes, por lo que tomaría más tiempo procesarlos. Probablemente sea linealmente proporcional al tamaño de los archivos. Desde mi experiencia limitada en grandes proyectos (donde el tiempo de compilación + enlace es lo suficientemente importante como para importar), el tiempo de enlace es casi insignificante comparado con el tiempo de compilación (a menos que sigas haciendo pequeños cambios y compilando, entonces esperaría que lo sintieras) , que supongo que puede suceder a menudo).
Actualizar
Esta fue la respuesta original de Real Slaw. Su respuesta anterior (la aceptada) es su segundo intento. Siento que su segundo intento responde la pregunta por completo. - Homer6
Bueno, para comparar, puedes buscar la idea de "compilación unificada" (nada que ver con el motor gráfico). Básicamente, una "compilación unitaria" es donde se incluyen todos los archivos cpp en un solo archivo y se compilan todos como una sola unidad de compilación. Creo que esto debería proporcionar una buena comparación, como AFAICT, esto es equivalente a hacer que tu proyecto sea solo encabezado. Te sorprendería el segundo "con" que enumeró; el objetivo de las "construcciones de unidades" es disminuir los tiempos de compilación. Supuestamente, las compilaciones unitarias se compilan más rápido porque:
.. son una forma de reducir el desarrollo de la construcción (específicamente abrir y cerrar archivos y reducir los tiempos de enlace al reducir el número de archivos de objeto generados) y, como tales, se usan para acelerar drásticamente los tiempos de construcción.
Comparación del tiempo de compilación (desde here ):
Tres referencias principales para "construcción unitaria":
Supongo que quieres razones para los pros y los contras de la lista.
Pros para solo encabezado
[...]
3) Puede ser mucho más rápido. (cuantificable) El código podría optimizarse mejor. La razón es que cuando las unidades están separadas, una función es simplemente una llamada a función y, por lo tanto, debe dejarse así. No se conoce información sobre esta llamada, por ejemplo:
- ¿Esta función modificará la memoria (y por lo tanto nuestros registros que reflejan esas variables / memoria quedarán obsoletos cuando vuelva)?
- ¿Esta función mira la memoria global (y por lo tanto no podemos reordenar a donde llamamos la función)
- etc.
Además, si se conoce el código interno de la función, podría valer la pena alinearlo (es decir, volcar su código directamente en la función de llamada). Inline evita la sobrecarga de llamada de función. La creación de líneas también permite que ocurra toda una serie de otras optimizaciones (por ejemplo, propagación constante, por ejemplo, llamamos factorial(10)
, ahora si el compilador no conoce el código de factorial()
, se ve obligado a dejarlo así , pero si conocemos el código fuente de factorial()
, podemos realmente variables las variables en la función y reemplazarlo por 10, y si tenemos suerte podemos incluso terminar con la respuesta en tiempo de compilación, sin ejecutar nada en absoluto en tiempo de ejecución). Otras optimizaciones después de la inclusión incluyen la eliminación de código muerto y (posiblemente) una mejor predicción de bifurcación.
4) Puede dar al compilador / enlazador mejores oportunidades para la optimización (explicación / cuantificable, si es posible)
Creo que esto se deduce de (3).
Contras para solo encabezado
1) Hincha el código. (cuantificable) (¿cómo afecta esto tanto el tiempo de ejecución como la huella de memoria?) El encabezado solo puede inflar el código de algunas maneras, que yo sepa.
El primero es la hinchazón de la plantilla; donde el compilador ejemplifica plantillas innecesarias de tipos que nunca se usan. Esto no es exclusivo de las plantillas, sino de los encabezados, y los compiladores modernos han mejorado en esto para que sea una preocupación mínima.
La segunda forma más obvia es la (sobre) delimitación de funciones. Si una función grande está insertada en todas partes, las funciones de llamada aumentarán de tamaño. Esto podría haber sido una preocupación sobre el tamaño del ejecutable y el tamaño de la memoria ejecutable-imagen hace años, pero el espacio en disco duro y la memoria han crecido para que sea casi inútil preocuparse. El problema más importante es que este aumento en el tamaño de la función puede arruinar el caché de instrucciones (de modo que la función ahora más grande no cabe en el caché, y ahora el caché debe rellenarse a medida que la CPU se ejecuta a través de la función). La presión de registro aumentará después de la alineación (hay un límite en el número de registros , la memoria en la CPU con la que la CPU puede procesar directamente). Esto significa que el compilador tendrá que hacer malabares con los registros en el medio de la función ahora más grande, porque hay demasiadas variables.
2) Tiempos de compilación más largos. (cuantificable)
Bueno, la compilación de solo encabezado lógicamente puede dar como resultado tiempos de compilación más largos por muchas razones (a pesar del rendimiento de "compilaciones unitarias"; la lógica no es necesariamente del mundo real, donde intervienen otros factores). Una razón puede ser que si un proyecto completo es solo encabezado, entonces perdemos compilaciones incrementales. Esto significa que cualquier cambio en cualquier parte del proyecto significa que se debe reconstruir todo el proyecto, mientras que con unidades de compilación separadas, los cambios en una cpp solo significan que el archivo de objeto debe reconstruirse y el proyecto volver a vincularse.
En mi experiencia (anecdótica), este es un gran éxito. El encabezado solo aumenta mucho el rendimiento en algunos casos especiales, pero en lo que respecta a la productividad, generalmente no vale la pena. Cuando comienza a obtener una base de código más grande, el tiempo de compilación desde cero puede tomar más de 10 minutos cada vez. Volver a comprar en un pequeño cambio comienza a ser tedioso. No sabes cuántas veces olvidé un ";" y tuve que esperar 5 minutos para escucharlo, solo para volver y arreglarlo, y luego esperar otros 5 minutos para encontrar algo más que acabo de presentar arreglando el ";".
El rendimiento es excelente, la productividad es mucho mejor; perderá gran parte de su tiempo y lo desmotivará / distraerá de su objetivo de programación.
Editar: Debo mencionar que la optimización interprocedural (véase también optimización de tiempo de enlace y optimización de todo el programa ) intenta lograr las ventajas de optimización de la "construcción unitaria". Las implementaciones de esto todavía son un poco inestables en la mayoría de los compiladores AFAIK, pero eventualmente esto podría superar las ventajas de rendimiento.
Resumen (puntos notables):
- Dos paquetes de puntos de referencia (uno con 78 unidades de compilación, una con 301 unidades de compilación)
- La compilación tradicional (compilación de unidades múltiples) dio como resultado una aplicación un 7% más rápida (en el paquete de 78 unidades); sin cambios en el tiempo de ejecución de la aplicación en el paquete de la unidad 301.
- Tanto la compilación tradicional como los puntos de referencia solo de encabezado usaron la misma cantidad de memoria cuando se ejecutaron (en ambos paquetes).
- La compilación solo de encabezado (compilación de unidad única) dio como resultado un tamaño ejecutable que era un 10% más pequeño en el paquete de 301 unidades (solo un 1% más pequeño en el paquete de 78 unidades).
- La compilación tradicional utilizaba aproximadamente un tercio de la memoria para compilar ambos paquetes.
- La compilación tradicional tardó tres veces más en compilarse (en la primera compilación) y tomó solo el 4% del tiempo en la compilación (ya que solo el encabezado debe recompilar todas las fuentes).
- La compilación tradicional tardó más en vincularse tanto en la primera compilación como en las compilaciones posteriores.
Punto de referencia de Box2D, datos:
Referencia de Botan, datos:
RESUMEN de Box2D (78 unidades)
Botan RESUMEN (301 Unidades)
BUENAS CARTAS:
Tamaño del archivo ejecutable Box2D:
Box2D compile / link / build / run time:
Box2D compila / enlaza / construye / ejecuta el uso máximo de la memoria:
Tamaño del ejecutable Botan:
Botan compile / link / build / run time:
Botan compile / link / build / run max uso de memoria:
Detalles de referencia
TL; DR
Los proyectos probados, Box2D y Botan fueron elegidos porque son potencialmente costosos desde el punto de vista computacional, contienen un buen número de unidades y en realidad tuvieron pocos o ningún error compilando como una sola unidad. Se intentaron muchos otros proyectos pero consumían demasiado tiempo para "arreglar" la compilación como una sola unidad. La huella de memoria se mide al sondear la huella de memoria a intervalos regulares y utilizando el máximo, y por lo tanto puede no ser totalmente precisa.
Además, este punto de referencia no genera automáticamente la dependencia de encabezado (para detectar cambios en el encabezado). En un proyecto que usa un sistema de compilación diferente, esto puede agregar tiempo a todos los puntos de referencia.
Hay 3 compiladores en el benchmark, cada uno con 5 configuraciones.
Compiladores:
- gcc
- icc
- sonido metálico
Configuraciones del compilador
- Predeterminado: opciones de compilador predeterminadas
- Nativo optimizado -
-O3 -march=native
- Tamaño optimizado -
-Os
- Nativo de LTO / IPO -
-O3 -flto -march=native
con clang y gcc,-O3 -ipo -march=native
con icpc / icc - Cero optimización -
-Os
Creo que cada uno puede tener diferentes orientaciones en las comparaciones entre compilaciones de unidades individuales y unidades múltiples. Incluí LTO / IPO para que podamos ver cómo se compara la forma "correcta" de lograr efectividad de unidad única.
Explicación de los campos csv:
-
Test Name
: nombre del punto de referencia. Ejemplos:Botan, Box2D
. - Configuración de prueba: nombre una configuración particular de esta prueba (indicadores cxx especiales, etc.). Usualmente es lo mismo que
Test Name
. -
Compiler
: nombre del compilador utilizado. Ejemplos:gcc,icc,clang
. -
Compiler Configuration
: nombre de una configuración de las opciones del compilador utilizadas. Ejemplo:gcc opt native
-
Compiler Version String
: primera línea de salida de la versión del compilador del compilador. Ejemplo:g++ --version
produceg++ (GCC) 4.6.1
en mi sistema. -
Header only
: un valor deTrue
si este caso de prueba se construyó como una sola unidad,False
si se construyó como un proyecto de varias unidades. -
Units
: número de unidades en el caso de prueba, incluso si está construido como una sola unidad. -
Compile Time,Link Time,Build Time,Run Time
, como suena. -
Re-compile Time AVG,Re-compile Time MAX,Re-link Time AVG,Re-link Time MAX,Re-build Time AVG,Re-build Time MAX
: los tiempos de reconstrucción del proyecto después de tocar un solo archivo. Cada unidad se toca, y para cada una, el proyecto se reconstruye. Los tiempos máximos y los tiempos promedio se registran en estos campos. -
Compile Memory,Link Memory,Build Memory,Run Memory,Executable Size
, según suenan.
Para reproducir los puntos de referencia:
- El bullwork es run.py
- Requiere psutil (para mediciones de huella de memoria).
- Requiere GNUMake.
- Tal como está, requiere gcc, clang, icc / icpc en la ruta. Se puede modificar para eliminar cualquiera de estos, por supuesto.
- Cada punto de referencia debe tener un archivo de datos que enumere las unidades de esos puntos de referencia. run.py creará dos casos de prueba, uno con cada unidad compilada por separado y otra con cada unidad compilada. Ejemplo: box2d.data . El formato de archivo se define como una cadena json, que contiene un diccionario con las siguientes claves
-
"units"
: una lista de archivosc/cpp/cc
que componen las unidades de este proyecto -
"executable"
: nombre del ejecutable que se compilará. -
"link_libs"
: una lista separada por espacios de bibliotecas instaladas para enlazar. -
"include_directores"
: una lista de directorios para incluir en el proyecto. -
"command"
- opcional. comando especial para ejecutar para ejecutar el punto de referencia. Por ejemplo,"command": "botan_test --benchmark"
-
- No todos los proyectos en C ++ pueden hacerse fácilmente; no debe haber conflictos / ambigüedades en la unidad individual.
- Para agregar un proyecto a los casos de prueba, modifique la lista
test_base_cases
en run.py con la información del proyecto, incluido el nombre del archivo de datos. - Si todo funciona bien, el archivo de salida
data.csv
debe contener los resultados del benchmark.
Para producir los gráficos de barras:
- Deberías comenzar con un archivo data.csv producido por el benchmark.
- Obtenga chart.py Requiere matplotlib .
- Ajuste la lista de
fields
para decidir qué gráficos producir. - Ejecute
python chart.py data.csv
. - Un archivo,
test.png
ahora debería contener el resultado.
Box2D
- Box2D se usó desde svn como está , revisión 251.
- El punto de referencia se tomó de here , se modificó here y podría no ser representativo de un buen punto de referencia de Box2D, y es posible que no use suficiente de Box2D para hacer este benchmark benchmark de referencia.
- El archivo box2d.data se escribió manualmente, al encontrar todas las unidades .cpp.
Botan
- Usando Botan-1.10.3 .
- Archivo de datos: botan_bench.data .
- Primero ejecuté
./configure.py --disable-asm --with-openssl --enable-modules=asn1,benchmark,block,cms,engine,entropy,filters,hash,kdf,mac,bigint,ec_gfp,mp_generic,numbertheory,mutex,rng,ssl,stream,cvc
, esto genera los archivos de encabezado y Makefile. - Inhabilité el ensamblaje, porque el ensamblado podría interferir con las optimizaciones que pueden ocurrir cuando los límites de las funciones no bloquean la optimización. Sin embargo, esto es una conjetura y podría ser totalmente erróneo.
- Luego ejecutó comandos como
grep -o "/./src.*cpp" Makefile
ygrep -o "/./checks.*" Makefile
para obtener las unidades .cpp y las puso en el archivo botan_bench.data . - Se modificó
/checks/checks.cpp
para no llamar a las pruebas de la unidad x509, y se eliminó la comprobación x509, debido al conflicto entre Botan typedef y openssl. - Se utilizó el punto de referencia incluido en la fuente de Botan.
Especificaciones del sistema:
- OpenSuse 11.4, 32 bits
- 4GB de RAM
-
Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz