Межпроцедурный анализ и оптимизация (1157497), страница 2
Текст из файла (страница 2)
/Qip[-] enable(DEFAULT)/disable single-file IP optimization within files
/Qipo[n] enable multi-file IP optimization between files
Ну и плюс еще есть две дополнительные опции, о которых я упомяну чуть позже.
/Qipo-c generate a multi-file object file (ipo_out.obj)
/Qipo-S generate a multi-file assembly file (ipo_out.asm)
Тут я позволю себе привести схему некоего абстрактного графа вызовов, характерного для C-шного проекта, чтобы статья смотрелась красивее работа компилятора с деревом вызовов для обоих методов выглядела более наглядно.
Формально можно сказать, что c –Qipo требуется больше ресурсов, а –Qip требует меньше ресурсов, но и обеспечивает худшее качество оптимизаций.
В реальности не все так однозначно, по крайней мере с ресурсами. Конечно, на самом деле графы вызовов для одного файла не являются подграфами графа вызовов для всего проекта. Например, графы для разных файлов могут содержать одни и те же функции, пришедшие из файлов описаний и являющиеся локальными для каждого файла. Особенно это актуально для больших C++ проектов. Такие функции могут быть продублированы в графах вызовов для некоторых файлов. На мой взгляд, условно можно считать, что эти функции являются неким аналогом weak функций. Поэтому, если просуммировать количество вершин для каждого отдельного графа при однофайловой модели оптимизации, то число вершин может получиться гораздо больше, чем число вершин в полном графе вызовов. При этом при однофайловой модели оптимизации все эти weak функции попадают в обработку и компилятор «возится» с каждой из них для того, чтобы затем линковщик оставил в исполняемом файле или библиотеке одну версию этой функции. Также количество работы при многофайловой обработке может быть уменьшено за счет удаления недостижимых из main подграфов и функций. Если функция была подставлена во все свои вызовы (и выполнены еще некоторые условия), то функция не будет обрабатываться оптимизирующей частью компилятора, т.е. итоговый исполняемый файл не будет содержать тела этой функции. Для однофайловой модели это верно только если функция имеет аттрибут static. Т.е при однофайловом IPA в обработку попадет какое-то количество «мертвых» функций.
Вообщем, может случиться, что в сумме компилятор выполнит гораздо больше работы с –Qip чем c –Qipo. При компиляции программы вместо одного большого графа вызовов придется обрабатывать много графов поменьше. В результате каждая однофайловая компиляция потребует гораздо меньше ресурсов, чем работа с полным графом вызовов, но количество истраченных ресурсов в сумме на компиляцию всех файлов может быть гораздо больше, чем при работе с полным графом вызовов.
Проиллюстрирую это уже упомянутым бенчмарком 447.dealII из CPU2006. Я уже написал, что в полном графе вызовов этого приложения приблизительно 320 000 вершин. В компиляции участвуют почти 120 файлов. Суммарное количество всех вершин в графах вызовов всех обрабатываемых файлов ~1500000 вершин (хотя в каждом файле вершин меньше 50000). Т.е. в 5 раз больше, чем количество всех вершин в полном графе вызовов. Я выключил все варианты параллельной компиляции и получил, что программа с –Qipo компилируется на моем Nehalemе приблизительно 160s, в то время как с –Qip время компиляции ~495s, т.е. в 3 раз больше. Если посчитать количество функций, которые обрабатываются в backendе с –Qipo, то получится ~19500 функций. Суммарное количество обработанных функций при компиляции проекта с –Qip равно ~55000. Ну и если посмотреть на размер выполняемого файла, то для этого приложения файл собранный с –Qipo почти на треть меньше файла собранного с –Qip.
Главным достоинством однофайловой оптимизации является то, что в этом случае при правке одного файла не нужно делать полного перестроения всего приложения, а перекомпилировать всего один файл и слинковать его с другими объектными файлами. Кроме того работу с –Qip легко распараллеливать, а вот создание приложения с –Qipo хуже поддается параллелизации. Хотя опция –Qipo может содержать числовой аргумент (например -Qipo2 подразумевает работу в двух потоках), но IPA выполняется на полном графе вызовов и пока еще не придуман хороший метод распараллеливания этого анализа, поэтому эта часть работы делается в один поток (или повторяется в каждом потоке) и кроме того каждый поток получает в нагрузку массу данных собранных IPA. Обработка всех функций в backendе может быть эффективно распределена между потоками, но из-за вышеперечисленных трудностей такая автоматическая параллелизация менее эффективна, чем в случае использования однофайловой модели.
Кстати говоря, хочется здесь упомянуть еще одну полезную опцию.
Если вы вдруг решили пойти простейшим путем и компилировать ваш проект с помощью одного вызова интеловского компилятора, передавая компилятору сразу все опции и список всех исходников, то по умолчанию компиляция будет осуществляться в один поток и нужно примнять специальную опцию для ускорения компиляции. Эта опция воздействует только на компиляцию без многофайлового IPA.
/MP[<n>] create multiple processes that can be used to compile large numbers of source files at the same time
Если упомянутый 447.deallI компилировать с различными значениями в –Qipo, то время компиляции будет меняться так: -Qipo1: 160s, -Qipo2: 136s, -Qipo4: 150s. Т.е. очень скоро потоки начинают мешать друг другу.
Если в том же вызове icl заменить –Qipo на –Qip (или убрать вообще, -Qip будет работать по умолчанию) и добавить опцию /MP, то получится следующее: /MP1: 495s; /MP2: 259s; /MP4: 145s; /MP8: 117s; /MP16: 84s
Если говорить о производительности, то многофайловый IPA намного эффективнее. (Хотя всегда существует некоторая вероятность «горя от ума»). Легко придумать простой пример, который бы продемонстрировал преимущества многофайловой модели в плане возможности использовать различные оптимизации благодаря дополнительной информации от IPA.
test_main1.c:
#include <stdio.h>
#include <stdlib.h>
#define N 10000
extern float calculate(float *a, float *b);
int main() {
float *a,*b,res;
int i;
a = (float*)malloc(N*sizeof(float));
b = (float*)malloc(N*sizeof(float));
for(i=0;i<N;i++) {
a[i] = i;
b[i] = 1 - i;
}
res = calculate(a,b);
printf("res = %f", res);
}
test1.c:
#include <stdio.h>
#define N 10000
extern float calc(float a, float b);
float calculate(float * restrict a,float * restrict b) {
int i;
float res;
for(i=0;i<N;i++) {
res = calc(a[i],b[i]);
}
return res;
}
test2.c:
float calc(float a,float b) {
return(a+b);
}
icl -Feaaa.exe -Qparallel -Qpar_report3 test_main.c test1.c test2.c -Qstd=c99
icl -Feaaa.exe -Qipo -Qparallel -Qpar_report3 test_main.c test1.c test2.c -Qstd=c99
Здесь используется опция –Qparallel, которая включает автопараллелизатор интеловского компилятора и –Qpar_report3 — опция, выдающая информацию о том, какие циклы были обработаны или отвергнуты автопараллелизатором и почему.
Вывод без –Qipo сообщает следующее:
...
test_main.c
procedure: main
..\test_main.c(14): (col. 1) remark: LOOP WAS AUTO-PARALLELIZED
test1.c
procedure: calculate
..\test1.c(11): (col. 1) remark: loop was not parallelized: existence of parallel dependence
test2.c
procedure: calc
Вывод с –Qipo:
test_main.c
test1.c
test2.c
procedure: main
C:\iusers\aanufrie\for_habrahabr\7a\test_main.c(13): (col. 1) remark: LOOP WAS AUTO-PARALLELIZED
C:\iusers\aanufrie\for_habrahabr\7a\test_main.c(17): (col. 8) remark: LOOP WAS AUTO-PARALLELIZED
Из вывода с –Qipo видно, что межпроцедурный анализ помог осуществить автопараллелизацию цикла.
Понятно, что в случае с –Qipo помогает еще и инлайнинг, но даже если инлайнинг запретить, то благодаря анализу функции calc у компилятора появляется возможность доказать правомерность автопараллелизации.
А что делать, если по какой-то причине многофайловая модель неприменима, например, для построения программы требуется много времени и это вас не устраивает? Можно повышать полезность работы однофайловой модели за счет размещения в файле совместно используемых функций. Очень полезен для функций атрибут static. Если функция имеет этот атрибут, значит IPA при однофайловой работе видит все вызовы этой функции и если у этих вызовов есть какие-то особенности, то они могут быть протянуты внутрь функции. Если все вызовы статической функции были подставлены, то само тело функции можно удалить. То же самое касается и статических объектов. Межпроцедурный анализ считает, что видит все примеры их использования и может выполнять с ними различные оптимизации. В общем, всякие ограничения области видимости объектов одним файлом могут помочь сделать более качественный однофайловый межпроцедурный анализ.
А что, если в рамках однофайловой нельзя решить какие-то проблемы, поскольку важная функциональность разнесена между несколькими файлами?
Опция –Qipo-c предоставляет возможность организовать некую промежуточную модель работы, т.е. строить граф вызовов не на основе функций из одного или всех файлов, а поделить все файлы на группы. Это удобно если ваш проект разбит на несколько тесно взаимосвязанных кусков. (Например, таким проектом является наш компилятор, в котором есть несколько компонент использующих общие утилиты и расположенные в отдельных директориях). –Qipo-c создает объектный файл с фиксированным именем ipo_out.obj и в этот файл попадают все функции из перечисленных в вызове компилятора файлов. В приведенном выше примере приложение можно было бы скомпилировать следующим образом:
icl -Qipo-c -Qparallel -Qpar_report3 -Qstd=c99 test1.c test2.c
icl test_main.c ipo_out.obj
Вывод при компиляции двух первых файлов показывает, что в данном случае будет и работать и автопараллелизация и инлайнинг, поскольку критически важные функции были объединены в одном графе вызовов, что и дало возможность сделать межпроцедурный анализ и оптимизации.
Вот такой вопрос, а использовал ли кто-либо из читателей такую схему компиляции или хотя бы слышал о ней? Есть такое мнение, что проблема производительности интересует только процентов 5% разработчиков. Зачастую желание предоставить пользователю какие-то дополнительные рычаги для управления работой компилятора упирается в простой вопрос: «А кому вообще это надо и кто это будет использовать?»
Кстати говоря, можно добавлять и удалять какие-то файлы из обработки межпроцедурным анализом используя метод «мух и котлет»: создавая объектные файлы с –Qipo и без. В этом случае межпроцедурный анализ будет проводится на неполном дереве, полученных из функций, которые описаны в файлах скомпилированных с –Qipo. В описанном мной примере это тоже работает.
Ну и наконец опция –Qipo-S полезна для тех, что хочет модернизировать или поизучать ассемблер после межпроцедурных оптимизаций.
На этом я закончу рассказ о графах вызовов как основе IPO и IPA. Надеюсь продолжить рассказ об межпроцедурных оптимизациях и постараюсь ответить на все вопросы.
9