Будет ли С++ компоновщик автоматически встроенными функциями (без ключевого слова "inline", без реализации в заголовке)?

Будет ли компоновщик С++ автоматически вводить "сквозные" функции, которые НЕ определены в заголовке, а NOT явно запрошено "встраиваться" через ключевое слово inline?

Например, следующее происходит так часто и всегда должно извлекаться из "inlining", что каждый поставщик компилятора должен "автоматически" обрабатывать его посредством "вставки" через компоновщик (в тех случаях, когда это возможно)

//FILE: MyA.hpp
class MyA
{
  public:
    int foo(void) const;
};

//FILE: MyB.hpp
class MyB
{
  private:
    MyA my_a_;
  public:
    int foo(void) const;
};

//FILE: MyB.cpp
// PLEASE SAY THIS FUNCTION IS "INLINED" BY THE LINKER, EVEN THOUGH
// IT WAS NOT IMPLICITLY/EXPLICITLY REQUESTED TO BE "INLINED"?
int MyB::foo(void)
{
  return my_a_.foo();
}

Я знаю, что компоновщик MSVS выполнит некоторую "вставку" с помощью своего кода генерации кода времени (LTGCC) и что инструментальная цепочка GCC также поддерживает оптимизацию времени связи (LTO) (см. Может ли встроенный компоновщик работать?).

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

Однако, если этот код связан с одним исполняемым файлом, который не пересекает границы DLL/shared-lib, я бы ожидал, что поставщик компилятора/компоновщика автоматически включит эту функцию в качестве простой и очевидной оптимизации ( пользуясь как производительностью, так и размером)?

Мои надежды слишком наивны?

Ответы

Ответ 1

Здесь вы можете быстро проверить свой пример (с реализацией MyA::foo(), которая просто возвращает 42). Все эти тесты были с 32-битными целями - возможно, что разные результаты можно увидеть с помощью 64-битных целей. Также стоит отметить, что использование опции -flto (GCC) или /GL (MSVC) приводит к полной оптимизации - везде, где вызывается MyB::foo(), она просто заменяется на 42.

С GCC (MinGW 4.5.1):

gcc -g -O3 -o test.exe myb.cpp mya.cpp test.cpp

вызов MyB:: foo() не был оптимизирован. MyB::foo() сам был слегка оптимизирован для:

Dump of assembler code for function MyB::foo() const:
   0x00401350 <+0>:     push   %ebp
   0x00401351 <+1>:     mov    %esp,%ebp
   0x00401353 <+3>:     sub    $0x8,%esp
=> 0x00401356 <+6>:     leave
   0x00401357 <+7>:     jmp    0x401360 <MyA::foo() const>

Какая запись пролога остается на месте, но сразу же отменяется (инструкция leave), а код переходит в MyA:: foo() для выполнения реальной работы. Однако это оптимизация, которую делает компилятор (а не компоновщик), поскольку он понимает, что MyB::foo() просто возвращает все MyA::foo(). Я не уверен, почему пролог остается.

MSVC 16 (от VS 2010) обрабатывал вещи несколько иначе:

MyB::foo() закончилось двумя прыжками: от одного до "thunk":

0:000> u myb!MyB::foo
myb!MyB::foo:
001a1030 e9d0ffffff      jmp     myb!ILT+0(?fooMyAQBEHXZ) (001a1005)

И thunk просто прыгнул на MyA::foo():

myb!ILT+0(?fooMyAQBEHXZ):
001a1005 e936000000      jmp     myb!MyA::foo (001a1040)

Опять же - это было в основном (полностью?), выполняемое компилятором, так как если вы посмотрите на код объекта, созданный до привязки, MyB::foo() скомпилирован до простого перехода к MyA::foo().

Итак, чтобы все это свалить - похоже, без явного вызова LTO/LTCG линкеры сегодня не хотят/не могут выполнить оптимизацию удаления вызова до MyB::foo() вообще, даже если MyB::foo() - это простой переход к MyA::foo().

Поэтому, если вы хотите оптимизировать время соединения, используйте -flto (для GCC) или /GL (для компилятора MSVC) и /LTCG (для компоновщика MSVC).

Ответ 2

Это распространено? Да, для основных компиляторов.

Автоматически? Обычно нет. Для MSVC требуется переключатель /GL, gcc и clang флаг -flto.

Как это работает? (только gcc)

Традиционный компоновщик, используемый в gcc toolchain, ld, и это немного глупо. Поэтому, и это может быть удивительно, оптимизация ссылок не выполняется компоновщиком в gch toolchain.

Gcc имеет специальное промежуточное представление, на котором выполняются оптимизации, которые являются агностиками языка: GIMPLE. При компиляции исходного файла с -flto (который активирует LTO) он сохраняет промежуточное представление в определенном разделе объектного файла.

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

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

Clang работает аналогично, и я предполагаю, что MSVC использует подобный трюк.

Ответ 3

Это зависит. Большинство компиляторов (линкеров, действительно) поддерживают такие оптимизации. Но для того, чтобы это было сделано, весь этап генерации кода в значительной степени должен быть отложен до времени ссылки. MSVC вызывает генерацию кода времени по умолчанию (LTCG), и по умолчанию она включена в версиях релизов, IIRC.

У GCC есть аналогичная опция под другим именем, но я не могу вспомнить, какие уровни -O, если они есть, разрешают это, или если она должна быть включена явно.

Однако, "традиционно", компиляторы С++ составляли единую единицу перевода отдельно, после чего компоновщик просто связывал свободные концы, гарантируя, что, когда блок перевода A вызывает функцию, определенную в блоке перевода B, правильная функция адрес просматривается и вставлен в вызывающий код.

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

Это не просто "простая" оптимизация, которая может быть выполнена "на лету", как, например, разворачивание цикла. Это требует, чтобы компоновщик и компилятор сотрудничали, потому что компоновщик должен взять на себя часть работы, обычно выполняемой компилятором.

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

Ответ 4

Вложение не является функцией компоновщика.

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

Это не делается по умолчанию, вы должны запросить его явно с соответствующими параметрами командной строки компилятору и компоновщику.

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

Ключевое слово inline действует только как руководство для компилятора для встроенных функций при оптимизации. В g++ уровни оптимизации -O2 и -O3 генерируют разные уровни вложения. В документе g++ doc указано следующее: (i) Если указано O2, то включенные функции -finline-small-functions будут включены. (Ii) Если указано O3, функции очереди включены вместе со всеми опциями для O2. (iii) Затем есть еще один подходящий вариант "no-default-inline", который будет включать функции-члены, только если добавлено ключевое слово "inline".

Как правило, размер функций (количество инструкций в сборке), если используются рекурсивные вызовы, определяет, происходит ли inlining. Есть много других опций, определенных в ссылке ниже для g++:

http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

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

Ответ 8

Вот мое понимание того, что компилятор будет делать с функциями:

Если определение функции находится внутри определения класса и не предполагает сценариев, которые предотвращают "встроенную" функцию, такую ​​как рекурсия, существует, функция будет "inline-d".

Если определение функции находится за пределами определения класса, функция не будет "inline-d", если в определении функции явно не включено ключевое слово inline.

Вот выдержка из Ivor Horton Начало Visual С++ 2010:

Встроенные функции

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

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

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

inline double CBox::Volume()
{
    return l * w * h;
}