Будет ли компоновщик C ++ автоматически встроенными функциями (без ключевого слова «inline», без реализации в заголовке)? - PullRequest
15 голосов
/ 29 августа 2011

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

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

//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) (см .: Может ли встроенный компоновщик функционировать? ).

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

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

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

Ответы [ 8 ]

18 голосов
/ 29 августа 2011

Вот краткий тест вашего примера (с реализацией 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().

Так что, если вы хотите оптимизировать время ссылки, используйте *Опции 1042 * (для GCC) или /GL (для компилятора MSVC) и /LTCG (для компоновщика MSVC).

11 голосов
/ 29 августа 2011

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

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

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

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

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

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

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

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

8 голосов
/ 29 августа 2011

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

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

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

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

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

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

5 голосов
/ 29 августа 2011

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

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

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

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

0 голосов
/ 29 августа 2011

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

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

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

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

0 голосов
/ 29 августа 2011

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

Если определение функции находится внутри определения класса и при условии отсутствия сценариев, которые препятствуют «встроенной» функции, такой как рекурсия, функция будет «inline-d».

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

Вот выдержка из Ivor Horton's Beginning Visual C ++ 2010:

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

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

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

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

inline double CBox::Volume()
{
    return l * w * h;
}
0 голосов
/ 29 августа 2011

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

0 голосов
/ 29 августа 2011

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

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

...