Являются ли оптимизации в LTO такими же, как при обычной компиляции?

При компиляции единицы перевода компилятор выполняет множество оптимизаций - встраивание, постоянное сгибание/распространение, анализ псевдонимов, разворот цикла, удаление мертвого кода и многие другие, о которых я даже не слышал. Все ли они выполняются при использовании LTO/LTCG/WPO между несколькими единицами перевода или просто подмножество (или вариант) из них сделано (я слышал о встраивании)? Если бы не все оптимизации были выполнены, я бы рассмотрел, что построения единичных процессоров превосходят LTO (или, возможно, их использование при наличии более 1 единства исходных файлов).

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

Документация по lto каждого компилятора точно не отвечает на это (или я не понимаю ее).

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

Обратите внимание, что я не спрашиваю о скорости сборки - это отдельная проблема.

ИЗМЕНИТЬ: Меня больше всего интересует gcc/llvm.

Ответ 1

Если вы посмотрите на документацию gcc, вы найдете:

-flto [= п]

Эта опция запускает стандартный оптимизатор ссылок времени. При вызове с исходным кодом он генерирует GIMPLE (одно из внутренних представлений GCC) и записывает его в специальные секции ELF в объектном файле. Когда объектные файлы связаны друг с другом, все тела функций считываются из этих разделов ELF и создаются так, как если бы они были частью одной и той же единицы перевода.

Чтобы использовать оптимизатор времени ссылки, параметры -flto и оптимизации следует указывать во время компиляции и во время финальной ссылки. Например:

          gcc -c -O2 -flto foo.c
          gcc -c -O2 -flto bar.c
          gcc -o myprog -flto -O2 foo.o bar.o

Первые два вызова в GCC сохраняют представление байт-кода GIMPLE в специальные секции ELF внутри foo.o и bar.o. Окончательный вызов читает байт-код GIMPLE из foo.o и bar.o, объединяет два файла в одно внутреннее изображение и компилирует результат, как обычно. Поскольку foo.o и bar.o объединены в одно изображение, , это заставляет все межпроцедурные анализы и оптимизации в GCC работать в двух файлах, как если бы они были одним. Это означает, например, что inliner может встроить функции в bar.o в функции в foo.o и наоборот.

Как говорится в документации, да, все! Оптимизация - это то, что программа скомпилирована в одном файле. Это также можно сделать с помощью -fwhole-program, чтобы получить "тот же" результат оптимизации.

Если вы скомпилируете этот очень простой пример:

f1.cpp:

int f1() { return 10; }

f2.cpp:

int f2(int i) { return 2*i; }

main.cpp:

int main()
{   
    int res=f1();
    res=f2(res);
    res++;

    return res;
} 

Я получил выход ассемблера:

00000000004005e0 <main>:
  4005e0:   b8 15 00 00 00          mov    $0x15,%eax
  4005e5:   c3                      retq   
  4005e6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4005ed:   00 00 00

Весь код встроен, как ожидалось.

Мой опыт в том, что фактический gcc оптимизируется с lto точно так же, как и скомпилирован в один файл. В очень редких условиях я получил ICE при использовании lto. Но с фактической версией 5.2.0 я больше не видел ICE.

[ICE] → Внутренняя ошибка компилятора