Подтвердить что ты не робот

Является ли связь С++ достаточно умной, чтобы избежать связывания неиспользуемых библиотек?

Я далек от полного понимания того, как работает компоновщик С++, и у меня есть конкретный вопрос об этом.

Скажем, у меня есть следующее:

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 */ }
}

main.cpp

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

Моя цель - генерировать как можно более мелкие двоичные файлы.

Мой вопрос: будет ли some_huge_lib включен в выходной файл объекта?

4b9b3361

Ответ 1

Включение или связывание с большими библиотеками обычно не будет иметь значения, если вы не используете этот материал. Линкеры должны выполнять удаление мертвого кода и, таким образом, гарантировать, что во время сборки вы не получите больших двоичных файлов с большим количеством неиспользуемого кода (прочитайте руководство для компилятора/компоновщика, чтобы узнать больше, это не применяется стандартом С++).

Включение большого количества заголовков также не увеличит ваш двоичный размер (но это может существенно увеличить время компиляции, предварительно скомпилированные заголовки). Некоторые исключения обозначают глобальные объекты и динамические библиотеки (их нельзя разделить). Я также рекомендую прочитать этот отрывок (только gcc) относительно разделения кода на несколько разделов.

Последнее уведомление об эффективности: если вы используете лот кода, зависящего от позиции (то есть код, который не может просто сопоставляться с каким-либо адресом с относительными смещениями, но требует некоторого "hotpatching" через перемещение или аналогичная таблица), тогда будет начальная стоимость.

Ответ 2

Это зависит от того, какие инструменты и коммутаторы вы используете для связи и компиляции.

Во-первых, если ссылка some_huge_lib как общая библиотека, все коды и зависимости необходимо будет решить при связывании разделяемой библиотеки. Так что да, его где-то затянут.

Если вы связываете some_huge_lib в качестве архива, тогда - это зависит. Хорошей практикой для здравого смысла читателя является создание func1 и func2 в отдельных файлах исходного кода, и в этом случае, вообще говоря, компоновщик сможет игнорировать неиспользуемые файлы объектов и их зависимости.

Если, однако, у вас есть обе функции в одном файле, некоторые компиляторы должны сказать им, чтобы они генерировали отдельные разделы для каждой функции. Некоторые компиляторы делают это автоматически, некоторые не делают этого вообще. Если у вас нет этой опции, потянув func1, вы получите весь код для func2, и все зависимости нужно будет решить.

Ответ 3

Подумайте о каждой функции как node в графе.
Каждый node связан с куском двоичного кода - скомпилированным двоичным кодом функции node.
Существует ссылка (направленная граница) между двумя узлами, если одна функция node (функция) зависит от (вызывает) другую.

Статическая библиотека - это в первую очередь список таких узлов (+ индекс).

Запуск программы node - это функция main().
Линкер пересекает график из main() и связывает в исполняемый файл все узлы, которые достижимы с main(). Именно поэтому он называется компоновщиком (привязка сопоставляет адреса вызовов функций в исполняемом файле).

Неиспользуемые функции, не имеют ссылок из узлов в графе, исходящих из main().
Таким образом, такие отсоединенные узлы недоступны и не включены в окончательный исполняемый файл.

Исполняемый файл (в отличие от статической библиотеки) представляет собой, прежде всего, список всех узлов, доступных из main() (+ индекс и код запуска среди прочего).

Ответ 4

В дополнение к другим ответам, нужно сказать, что обычно линкеры работают в терминах секций, а не функций.

Компиляторы обычно настраивают, помещают ли они весь ваш объектный код в один монолитный раздел или разделяют на несколько меньших. Например, параметры GCC для включения расщепления - это -ffunction-sections (для кода) и -fdata-sections (для данных); Опция MSVC /Gy (для обоих). -fnofunction-sections, -fnodata-sections, /Gy- соответственно, чтобы поместить весь код или данные в один раздел.

Вы можете "играть" с компиляцией своих модулей в обоих режимах, а затем сбрасывать их (objdump для GCC, dumpbin для MSVC), чтобы увидеть сгенерированную структуру объектных файлов.

Как только раздел формируется компилятором, для компоновщика это единица. Разделы определяют символы и ссылаются на символы, определенные в других разделах. Компилятор построит график зависимостей между разделами (начиная с нескольких корней), а затем либо расформирует, либо сохранит каждый из них целиком. Итак, если у вас есть неиспользуемая и неиспользуемая функция в разделе, неиспользуемая функция будет сохранена.

В обоих режимах есть и преимущества, и недостатки. Превращение разбиения означает меньшие исполняемые файлы, но более крупные объектные файлы и более длительные временные привязки.

Следует также отметить, что в С++, в отличие от C, существуют определенные ситуации, когда правило одного определения ослабляется, и допускаются множественные определения функции или объекта данных (например, в случае встроенных функций). Правила формулируются таким образом, что компоновщику разрешено выбирать любое определение.

С точки зрения разделов, включение встроенных функций вместе с нестрочными, означало бы, что в типичном сценарии использования компоновщик обычно должен принудительно сохранять практически каждое определение каждой встроенной функции; это будет означать чрезмерное раздувание кода. Поэтому такие функции и данные обычно помещаются в свои разделы независимо от параметров командной строки компилятора.

ОБНОВЛЕНИЕ: Как правильно напомнил @janm в своем комментарии, линкеру также следует дать указание избавиться от незаписанных разделов, указав --gc-sections (GNU) или /opt:ref (MS).