Оптимизация времени соединения и встраивание

По моему опыту, есть много кода, который явно использует встроенные функции, которые идут на компромисс:

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

Вопрос заключается в следующем: делает ли оптимизация во время соединения (например, в GCC) ручное встраивание, например, объявление в C99 функции "встроенной" и обеспечение реализации, устаревшей? Правда ли, что нам не нужно учитывать встроенные функции для большинства функций самим? Как насчет функций, которые всегда извлекают выгоду из встраивания, например, deg_to_rad(x)?

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

Обновление: я часто видел оппозицию против "встроенного", и это было предложено устаревшим. Лично, однако, я часто вижу явно встроенные функции: как функции, определенные в теле класса.

5 ответов

Даже с LTO, компилятор все еще должен использовать эвристику, чтобы определить, стоит ли включать функцию для каждого вызова (обратите внимание, он принимает решение не по функции, а по вызову). Эвристика учитывает такие факторы, как - находится ли он в цикле, развернут ли цикл, насколько велика функция, как часто она вызывается глобально и т. Д. Компилятор, безусловно, никогда не сможет точно определить, как часто вызывается код, и может ли расширение кода уничтожить кэши инструкций / трассировки / цикла / микрокода конкретного ЦП во время компиляции.

Предполагается, что оптимизация по профилю - это шаг к решению этой проблемы, но если вы когда-либо пробовали это сделать, вы, вероятно, заметили, что вы можете получить скачок в производительности порядка 0-2%, и это может быть в в любом направлении!:-) Это все еще в стадии разработки.

Если производительность является вашей конечной целью, и вы действительно знаете, что делаете, и действительно проводите тщательный анализ своего кода, то на самом деле нужен способ указать компилятору встроить или не встроить для каждого вызова отдельно, а не подсказка для каждой функции. На практике я справился с этим, используя специфичные для компилятора подсказки типа "force_no_inline" для случаев, когда я не хочу вставлять, и отдельную копию "force_inline" (или макрос в редком случае, если это не удается) функции, когда я хочу, чтобы она была встроенной, Если кто-нибудь знает, как сделать это более понятным способом с помощью специальных подсказок компилятора (для любых компиляторов C/C++), пожалуйста, дайте мне знать.

Чтобы конкретно адресовать ваши очки:

1. Код становится менее лаконичным и несколько менее понятным.

Как правило, нет - это просто подсказка ключевого слова, которая управляет тем, как оно встроено. Однако, если вы прыгаете через обручи, как я описал в предыдущем абзаце, тогда да.

2. Иногда встраивание может значительно увеличить производительность во время выполнения.

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

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

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

GCC 9 Binutils 2.33 эксперимент, чтобы показать, что LTO может встроить

Для тех, кому интересно, если ld встроены в объектные файлы или нет, вот быстрый эксперимент, подтверждающий, что он может:

main.c

int notmain(void);

int main(void) {
    return notmain();
}

notmain.c

int notmain(void) {
    return 42;
}

Скомпилировать с помощью LTO и разобрать:

gcc -O3 -flto -ggdb3 -std=c99 -Wall -Wextra -pedantic -c -o main.o main.c
gcc -O3 -flto -ggdb3 -std=c99 -Wall -Wextra -pedantic -c -o notmain.o notmain.c
gcc -O3 -flto -ggdb3 -std=c99 -Wall -Wextra -pedantic -o main.out notmain.o main.o
gdb -batch -ex "disassemble/rs main" main.out

Выход:

   0x0000000000001040 <+0>:     b8 2a 00 00 00  mov    $0x2a,%eax
   0x0000000000001045 <+5>:     c3      retq 

Так что да нет callq, встроенный.

Без -flto:

   0x0000000000001040 <+0>:     f3 0f 1e fa     endbr64 
   0x0000000000001044 <+4>:     e9 f7 00 00 00  jmpq   0x1140 <notmain>

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

Основным недостатком определения в заголовках является то, что они могут замедлить компиляцию. Для шаблонов C++ вас также может заинтересовать явное создание экземпляра шаблона: Явное создание экземпляра шаблона - когда оно используется?

Проверено в Ubuntu 19.10.

Вопрос заключается в следующем: делает ли оптимизация во время соединения (например, в GCC) ручное встраивание, например, объявление в C99 функции "встроенной" и обеспечение реализации, устаревшей?

Эта статья, кажется, отвечает "Да:"

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

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

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

  1. Я не думаю, что встроенное ключевое слово влияет на ремонтопригодность, и лишь краткость. (Мнение)
  2. Иногда встроенный может снизить производительность во время выполнения: http://www.parashift.com/c++-faq-lite/inline-functions.html
  3. Компиляторы достаточно умны в отношении встраивания, я слышал, что Visual Studio почти полностью игнорирует их и принимает решение о встраивании.

does link-time optimization render manual inlining, obsolete?Вовсе нет, оптимизатор, который делает встроенное ключевое слово почти устаревшим, начинает работать раньше, чем время соединения.

Item 33 - Scott Myers - 2nd Ed - Эффективные C++ приходят на ум.

Вы должны иметь в виду ключевое слово static в строке! Теперь есть гнездо шершней!

Другие вопросы по тегам