¿Es la vinculación de C++ lo suficientemente inteligente como para evitar la vinculación de bibliotecas no utilizadas?

7 minutos de lectura

avatar de usuario
idanshmu

Estoy lejos de entender completamente cómo funciona el enlazador de C++ y tengo una pregunta específica al respecto.

Digamos que tengo lo siguiente:

Utils.h

namespace Utils
{
    void func1();
    void func2();
}

Utils.cpp

#include "some_huge_lib" // Needed only by func2()

namespace Utils
{
    void func1() { /* Do something */ }
    void func2() { /* Make use of some functions defined in some_huge_lib */ }
}

principal.cpp

int main()
{
    Utils::func1()
}

Mi objetivo es generar archivos binarios tan pequeños como sea posible.

Voluntad some_huge_lib incluirse en el archivo de objeto de salida?

  • El enlazador vinculará los cuerpos de función que se han llamado, no el conjunto completo de otras funciones que no se han llamado.

    – Dra. Debasish Jana

    08/09/2014 a las 10:10

  • El enlazador incluye solo las funciones que se han llamado en su código. Otras funciones se evitan y no se incluyen en el archivo de objeto final, que yo sepa.

    – Ufuk Can Bicici

    8 sep 2014 a las 10:11

  • ¿Qué pasa si #include “some_huge_lib” se declaró en utils.h, hace alguna diferencia?

    – idanshmu

    8 sep 2014 a las 10:12

  • Es importante destacar que The Standard no especifica esto, por lo que para saber exactamente qué comportamiento esperar, debe decir qué plataforma, compilador, banderas, etc.

    – Bobtfish

    08/09/2014 a las 10:15


  • Mira este: stackoverflow.com/questions/6687630/…

    – Robert Mutke

    8 sep 2014 a las 10:19

avatar de usuario
Marco A.

Incluir o enlazar con bibliotecas grandes generalmente no hará una diferencia a menos que usar Esas cosas. Enlazadores debería realice la eliminación de código inactivo y, por lo tanto, asegúrese de que en el momento de la compilación no obtenga archivos binarios grandes con una gran cantidad de código sin usar (lea el manual de su compilador/enlazador para obtener más información, esto no lo aplica el estándar C++).

Incluir muchos encabezados tampoco aumentará su tamaño binario (pero podría aumentar sustancialmente su tiempo de compilación, cfr. encabezados precompilados). Algunas excepciones representan objetos globales y bibliotecas dinámicas (no se pueden eliminar). También recomiendo leer este pasaje (solo gcc) con respecto a la separación del código en varias secciones.

Un último aviso sobre las actuaciones: si usas un lote del código dependiente de la posición (es decir, el código que no puede asignarse a ninguna dirección con compensaciones relativas, sino que necesita un “hotpatching” a través de una reubicación o una tabla similar), entonces habrá un costo inicial.

  • esto solo funciona para enlaces estáticos. las bibliotecas compartidas deben contener símbolos visibles. para CCG: gcc.gnu.org/ml/gcc-help/2003-08/msg00128.html

    – Alejandro Ah

    08/09/2014 a las 10:31

  • ¿Siempre ha sido así? Creo que tenía un compilador a principios de los 90 que incluía todo, usado o no. Microsoft Quick C 2.0. en.wikipedia.org/wiki/QuickC

    – kmort

    08/09/2014 a las 15:05

  • Creo que solo cuando se agregó WPO, incluso entonces no es tan “inteligente” en realidad. Lance algunas rutas complejas “quizás” a una cadena de funciones y no las eliminará, incluso si nunca se pueden llamar.

    – Paula

    08/09/2014 a las 20:15

  • No todos los enlazadores hacen esto y depende de cómo se compilaron las cosas y de la inteligencia del enlazador. Solía ​​haber un mercado para los enlazadores que hacían esto cuando los enlazadores estándar no lo hacían. En el mundo moderno necesitas compilar con -ffunction-sections en gcc y enlace con --gc-sections. Para VC++ necesitas compilar con /Gy y enlace con /opt:ref

    – janm

    9 de septiembre de 2014 a las 6:30

Esto depende de un lote sobre qué herramientas e interruptores usa para vincular y compilar.

En primer lugar, si el enlace some_huge_lib como biblioteca compartida, todo el código y las dependencias deberán resolverse al vincular la biblioteca compartida. Así que sí, se detendrá en alguna parte.

si vinculas some_huge_lib como archivo, entonces – depende. Es una buena práctica para la cordura del lector colocar func1 y func2 en archivos de código fuente separados, en cuyo caso, en general, el enlazador podrá ignorar los archivos de objetos no utilizados y sus dependencias.

Sin embargo, si tiene ambas funciones en el mismo archivo, necesitará, en algunos compiladores, decirles que produzcan secciones individuales para cada función. Algunos compiladores hacen esto automáticamente, otros no lo hacen en absoluto. Si no tiene esta opción, extraer func1 extraerá todo el código para func2, y será necesario resolver todas las dependencias.

  • Esta es la respuesta correcta. Los diferentes enlazadores se comportan de manera diferente y se ven afectados por las banderas utilizadas.

    –Jorgen Fogh

    08/09/2014 a las 18:03

  • Acordado. Por experiencia, esta es la respuesta correcta.

    – Fred Mitchell

    10/09/2014 a las 22:10

  • ¿Podría el votante negativo explicar por qué, para que pueda mejorar la respuesta si es posible?

    – Tom Tanner

    5 de noviembre de 2014 a las 11:19


avatar de usuario
Adi Shavit

Piense en cada función como un nodo en un gráfico.
Cada nodo está asociado con una pieza de código binario: el binario compilado de la función del nodo.
Hay un enlace (borde dirigido) entre 2 nodos si un nodo (función) depende (llama) a otro.

Una biblioteca estática es principalmente una lista de dichos nodos (+ un índice).

El programa nodo de inicio es el main() función.
los enlazador atraviesa la gráfica de main() y Enlaces en el ejecutable todos los nodos que son accesibles desde main(). Por eso se llama un enlazador (el enlace mapea las direcciones de llamada de función dentro del ejecutable).

Funciones no utilizadas, no tienen enlaces de nodos en el gráfico que emanan de main().
Por lo tanto, dichos nodos desconectados no son accesibles y no se incluyen en el ejecutable final.

El ejecutable (a diferencia de la biblioteca estática) es principalmente una lista de todos los nodos accesibles desde main() (+ un índice y código de inicio entre otras cosas).

  • Una explicación muy gráfica. Me gusta esta respuesta.

    – Pavlo Dybán

    11 de septiembre de 2014 a las 10:15

  • @PavloDyban: ¡Gracias! 🙂

    – Adi Shavit

    11 de septiembre de 2014 a las 10:16

avatar de usuario
ach

Además de otras respuestas, hay que decir que normalmente los enlazadores funcionan en términos de secciones, no de funciones.

Los compiladores generalmente lo tienen configurable ya sea que coloquen todo su código de objeto en una sección monolítica o lo dividan en varios más pequeños. Por ejemplo, las opciones de GCC para activar la división son -ffunction-sections (para el código) y -fdata-sections (para datos); La opción MSVC es /Gy (para ambos). -fnofunction-sections, -fnodata-sections, /Gy- respectivamente para poner todo el código o datos en una sección.

Puede ‘jugar’ con la compilación de sus módulos en ambos modos y luego descargarlos (objdump para CCG, dumpbin para MSVC) para ver la estructura del archivo de objeto generado.

Una vez que el compilador forma una sección, para el enlazador es una unidad. Las secciones definen símbolos y se refieren a símbolos definidos en otras secciones. El enlazador creará un gráfico de dependencia entre las secciones (comenzando en un número de raíces) y luego disolverá o conservará cada una de ellas por completo. Por lo tanto, si tiene una función usada y una sin usar en una sección, la función sin usar se mantendrá.

Hay ventajas y desventajas en cualquiera de los dos modos. Activar la división significa archivos ejecutables más pequeños, pero archivos de objetos más grandes y tiempos de vinculación más prolongados.

También se debe tener en cuenta que en C++, a diferencia de C, hay ciertas situaciones en las que la regla de definición única se relaja y se permiten múltiples definiciones de una función u objeto de datos (por ejemplo, en el caso de funciones en línea). Las reglas están formuladas de tal manera que el enlazador puede elegir cualquier definición.

Desde el punto de vista de las secciones, juntar las funciones en línea con las no en línea significaría que, en un escenario de uso típico, el enlazador normalmente se vería obligado a mantener prácticamente todas las definiciones de todas las funciones en línea; eso significaría una excesiva hinchazón del código. Por lo tanto, dichas funciones y datos normalmente se colocan en sus propias secciones, independientemente de las opciones de la línea de comandos del compilador.

ACTUALIZAR: Como @janm recordó correctamente en su comentario, también se debe indicar al enlazador que se deshaga de las secciones sin referencia especificando --gc-sections (GNU) o /opt:ref (EM).

¿Ha sido útil esta solución?