280 likes | 451 Views
Межпроцедурные оптимизации. Межпроцедурный анализ. Как совместить хороший стиль программирования и требования к быстродействию приложения? Модульность. Читаемость кода и использование подпрограмм для повторяющихся вычислений. Принцип реализации утилит как «черного ящика».
E N D
Межпроцедурный анализ Как совместить хороший стиль программирования и требования к быстродействию приложения? • Модульность. • Читаемость кода и использование подпрограмм для повторяющихся вычислений. • Принцип реализации утилит как «черного ящика». Модульность исходного кода усложняет задачу по его оптимизации. Обсуждаемые в предыдущих разделах оптимизации процедурного уровня: • эффективно работают только с локальными переменными; • всякий вызов функции - «черный ящик»; • неизвестны многие свойства переданных в процедуру параметров; • неизвестны свойства глобальных переменных. Для решения этих проблем необходимо исследование программы в целом.
Некоторые основные проблемы оптимизаций процедурного уровня 1) Скалярные оптимизации. Reaches(b) = U для всех предшественников (defsout(p) U (reaches(p) ∩ ¬killed(p)) В случае вызова неизвестной функции в базовом блоке p необходимо все глобальные переменные, локальные переменные и доступные функции, поместить в killed(p). Для качественных скалярных оптимизаций необходимы знания о свойствах функций, вызываемых внутри процедуры. 2) Оптимизации циклических конструкций. Для таких оптимизаций необходимо: • корректное определение объектов, которые могут ссылаться на одну память; • знание свойств функций внутри циклов (не изменяют итерационные переменные, не содержат выхода из программы и т.д.); • оценка количества итераций цикла. 3) Векторизация. Необходима информация о выравнивании объектов в памяти.
Протяжка константы через неизвестную функцию Рассмотрим простую программу: test.c: extern void unknown(int *a); int main(){ int a,b,c; a=5; c=a; unknown(&a); if(a==5) printf("a==5\n"); b=a; printf("%d %d %d\n",a,b,c); return(1); } Сохранится ли if утверждение в результирующем коде?
Ассемблер полученный с помощью icl:icс –O2 test.c –S … call_unknown ;9.1 ; LOE ebxesi .B1.9: ; Preds .B1.8 addesp, 8 ;9.1 ; LOE ebxesi .B1.2: ; Preds .B1.9 movedi, DWORD PTR [a.3.0.1] ;10.4 cmpedi, 5 ;10.7 jne .B1.4 ; Prob 0% ;10.7 ; LOE ebxesiedi … Вывод: В общем случае, когда о вызываемой функции ничего не известно, константа, присвоенная переменной, не протягивается через функцию, которая может изменить значение этой переменной.
CSE (Удаление общих подвыражений) #include <math.h> #include <stdio.h> externvoidunknown(); floata,b; intmain() { floatc,d; scanf_s("%f",&a); scanf_s("%f",&b); c=0; if(sqrt(a+b)>3) c=a+b; else unknown(); d=sqrt(a+b)+c; printf("d=%f\n",d); return 1; } Здесь есть общее подвыражение sqrt(a+b). Будет ли CSE работать с этим подвыражением?
icl cse.c –S … .B1.3:: ; Preds .B1.2 movss xmm0, DWORD PTR [a] ;15.9 pxor xmm14, xmm14 ;14.1 addss xmm0, DWORD PTR [b] ;15.11 cvtps2pd xmm1, xmm0 ;15.11 sqrtsd xmm1, xmm1 ;15.4 comisd xmm1, QWORD PTR [_2il0floatpacket.0] ;15.14 jbe .B1.5 ; Prob 22% ;15.14 ; LOE rbx rbp rsi rdi r12 r13 r14 r15 xmm0 xmm1 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 .B1.4:: ; Preds .B1.3 movaps xmm14, xmm0 ;16.3 jmp .B1.7 ; Prob 100% ;16.3 ; LOE rbx rbp rsi rdi r12 r13 r14 r15 xmm1 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 .B1.5:: ; Preds .B1.3 call unknown ;19.3 ; LOE rbx rbp rsi rdi r12 r13 r14 r15 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 .B1.6:: ; Preds .B1.5 movss xmm0, DWORD PTR [a] ;21.8 addss xmm0, DWORD PTR [b] ;21.10 cvtps2pd xmm1, xmm0 ;21.10 sqrtsd xmm1, xmm1 ;21.3 …
Однопроходная и двухпроходная компиляция Для того, чтобы собрать информацию о свойствах функций, необходим дополнительный проход. Но, поскольку каждая функция, в свою очередь, может вызывать другие функции, а также себя (рекурсия), то необходимо произвести анализ графа вызовов (Call graph). Граф вызовов представляет взаимоотношение вызовов между процедурами в программе. Каждая вершина представляет процедуру, и каждая грань (f,g) указывает, что процедура f вызывает процедуру g. Граф может быть статическим, вычисленным на этапе компиляции или динамическим, т.е. отражающим реальные вызовы при выполнении программы. (VTune). Одной из основных задач межпроцедурного анализа является построения графа вызовов и выяснения свойств функций на основе его анализа (например, глобальный анализ потоков данныхтребует знания о том, какие данные каждая функция модифицирует). Граф вызовов может быть полным и неполным. Если при сборе проекта используются библиотеки, свойства функций которых мы не знаем, то граф будет являться неполным, и полноценный анализ не будет осуществлен.
Исходные файлы FE (C++/C или Fortran) Архитектура компилятора Внутреннее представление Профилировщик Временный файл или Obj с ВП Скалярные оптимизации HPO IP/IPO оптимизации Генератор кода Скалярные оптимизации Обьектные файлы HPO Генератор кода Исполняемый файл Библиотека
Два вида межпроцедурных оптимизаций: • модульная • оптимизация для всей программы 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) /Qipo-jobs<n> specify the number of jobs to be executed simultaneously during the IPO link phase
Изменится ли что-либо, если мы определим некую процедуру unknown и перекомпилируем с –ipo: #include <stdio.h> externfloata,b; voidunknown() { printf("a=%fb=%f\n",a,b); } icl –Qipotest.cunknown.c –Ob0 –Qipo-S Работают ли протяжка констант и удаление общих подвыражений?
Анализ совмещений (alias analysis) • анализ совмещения через параметры; • анализ совмещения указателей для глобальных и статических указателей (Local point to analysis - LPT). • Важная часть механизма определения зависимостей. • В случае с анализом совмещения указателей с каждым указателем связывается множество возможных значений. Два указателя могут ссылаться на одну область памяти, если пересечение множеств их допустимых значений не пусто.
Пример на LPT анализ • #include <stdio.h> • int p1=1,p2=2; • int *a,*b; • void init(int **a, int **b) { • *a=&p1; • *b=&p1; // <= a and b poins to p1 • } • int main() { • int i,ar[100]; • init(&a,&b); • printf("*a= %d *b=%d\n",*a,*b); • for(i=0;i<100;i++) { • ar[i]=i*(*a)*(*a); • *b+=1; // *a is changed through *b • } • printf("ar[50]= %d p2=%d\n",ar[50],p2); • } • Можно ли осуществлять цикловые оптимизации с циклом for?
Межпроцедурный анализ используется для протяжки атрибутов функций. Например, есть атрибуты no_side_effect, always_return и т.д. IPA используется для протяжки атрибутов переменных. Например, переменные, помеченные атрибутом «адрес был взят», исключаются из многих оптимизаций. Этот атрибут для глобальных переменных устанавливает IPA. Продвижение данных (Data promotion). Каждая переменная имеет свою область видимости (scope). IPA позволяет протягивать данные, которые используются только в определенной процедуре, на уровень этой процедуры, что сразу позволяет включить их во многие оптимизации. Функции, используемые только в одной программе, получают атрибут static. Удаление неиспользуемых глобальных переменных. Удаление мертвого кода. В данном случае, функция не будет включаться в исполняемый модуль, если она не входит в граф вызовов или все ее вызовы были подставлены (inline) (вывод – для улучшения размеров кода и времени компиляции используйте атрибут static). Протяжка информации о выравнивании аргументов. Если актуальные аргументы функции всегда выровнены, то мы можем улучшить векторизацию внутри процедуры.
Межпроцедурные оптимизации - используются связи, возникающие при вызовах процедур, для того, чтобы оптимизировать одну или несколько процедур или выяснить, как они соотносятся друг с другом. Протяжка константных аргументов Если при каждом вызове процедуры f(i,j,k) в программе в качестве аргумента i всегда передается некая константа, то это позволяет присвоить первому формальному аргументу значение этой константы в коде процедуры f(). Протяжка возвращаемых значений. Если процедура всегда возвращает константное значение, то возможно протянуть это значение наружу. То же самое касается передаваемых в процедуру аргументов. Если перед выходом из процедуры значение аргумента константа, то ее также можно протянуть наружу.
cat known.c void known(int var,int *ttt) { if(var>0) (*ttt)++; else (*ttt)--; } Пример. Константа протягивается в функцию через аргумент. Константа протягивается в вызывающую функцию. cat test.c #include <stdio.h> externvoidknown(intvariant,int *var); intmain() { intvar; intttt; var=2; ttt=3; known(var,&ttt); printf("ttt=%i\n",ttt); } icc –Ob0 test.c known.c -fast -ipo-S … known: # parameter 1: %edi # parameter 2: %rsi ..B2.1: # Preds ..B2.0 ..___tag_value_known.8: #1.30 addl $1, (%rsi) #3.3 ret #6.1 .align 16,0x90 … Таким образом, протянув константу удалось избавиться от ветвления.
Подстановка (inlining) Подстановка удаляет накладные расходы, связанные с подготовкой к вызову функции, удаляет переходы, которые могут быть источниками неэффективной работы памяти, позволяет эффективнее применять скалярные и оптимизации циклов.Недостаток оптимизации – увеличение размера приложения. Как следствие – увеличение времени компиляции и необходимых для компиляции ресурсов. Эвристики для подстановок пытаются выбрать наиболее выгодные для подстановки функции, чтобы получить максимальный эффект для производительности, не выходя за пределы допустимого увеличения кода. Атрибут функции inline inline int exforsys(int x1) { return 5*x1;} Программист «рекомендует» компилятору сделать подстановку такой функции.
Управление подстановкой • Синтаксис: • #pragma inline[recursive] • #pragma forceinline[recursive] • #pragma noinline • Аргумент Recursive • требует чтобы данная директива применялась ко всем вызовам, которые осуществляются данным вызовом. • Директива inline рекомендует инлайнить • noinline требует не инлайнить • forceinline требует инлайнить • Аналоги для языка Фортран • cDEC$ ATTRIBUTES INLINE :: procedure • cDEC$ ATTRIBUTES NOINLINE :: procedure • cDEC$ ATTRIBUTES FORCEINLINE :: procedure
Компиляторные опции подстановки • /Ob<n> control inline expansion: • n=0 disable inlining • n=1 inline functions declared with __inline, and perform C++inlining • n=2 inline any function, at the compiler's discretion • /Qinline-min-size:<n> • set size limit for inlining small routines • /Qinline-min-size- • no size limit for inlining small routines • /Qinline-max-size:<n> • set size limit for inlining large routines • /Qinline-max-size- • no size limit for inlining large routines • /Qinline-max-total-size:<n> • maximum increase in size for inline function expansion • /Qinline-max-total-size- • no size limit for inline function expansion
/Qinline-max-per-routine:<n> • maximum number of inline instances in any function • /Qinline-max-per-routine- • no maximum number of inline instances in any function • /Qinline-max-per-compile:<n> • maximum number of inline instances in the current compilation • /Qinline-max-per-compile- • no maximum number of inline instances in the current compilation • /Qinline-factor:<n> • set inlining upper limits by n percentage • /Qinline-factor- • do not set set inlining upper limits • /Qinline-forceinline • treat inline routines as forceinline • /Qinline-dllimport • allow(DEFAULT)/disallow functions declared __declspec(dllimport) to be inlined • /Qinline-calloc directs the compiler to inline calloc() calls as malloc()/memset()
Клонирование функций Если в функцию f(x,y,x) передается x=2 в одном случае и x=3 в другом, то возможно заменить вызов функции f на вызовы f2и f3. Частичная подстановка Если в функции f содержится вначале функции код, зависящий только от формальных аргументов, то этот код может быть подставлен в вызывающую функцию и удален из функции f.
Девиртуализация для C++ Вызов функций через указатели дороже простого вызова. C++ - объектно-ориентированный язык, поддерживающий высокий уровень абстракции. Возможность выполнения методов функции в зависимости от типа объекта времени выполнения. A => B => C Все наследуемые классы переопределяют virtual int foo() int process(class A *a) { return(a->foo()); }
icl -S rta0.cc Рассмотрим ассемблер для функции process: ?process@@YAHPAVA@@@Z PROC NEAR ; parameter 1: 4 + esp .B4.1: ; Preds .B4.0 mov ecx, DWORD PTR [4+esp] ;13.5 mov eax, DWORD PTR [ecx] ;14.9 call DWORD PTR [eax] ;14.9 .B4.2: ; Preds .B4.1 ret ;14.9 ALIGN 16 ; LOE ; mark_end; ?process@@YAHPAVA@@@Z ENDP #include <stdio.h> class A { virtual int foo() { return 1; }; friend int process(class A *a); }; class B: public A { virtual int foo() { return 2; }; friend int process(class A *a); }; int process(class A *a) { return(a->foo()); }; void main() { A* pA = new A; B* pB = new B; int result1 = process(pA); int result2 = process(pB); printf("%d,%d\n",result1,result2); }
Трансформации данных Перестановка полей структур. Если несколько полей используются в программе очень интенсивно, то, разместив их рядом, можно снизить количество промахов по памяти. Расщепление структуры (structure splitting). Редко используемые поля выносятся в специальную «холодную» секцию. Без межпроцедурного анализа нельзя доказать возможность таких оптимизаций.
Перестановка полей и разбиение структуры struct.h: #ifndef PERF typedef struct { double x; char title[40]; double y; char title2[22]; double z; } VecR; #else typedef struct { char title[40]; char title2[22]; } ColdFields; typedef struct { double x; double y; double z; ColdFields *cold; } VecR; #endif
icc struct.c -fast -o a.out icc struct.c -fast -DPERF -o b.out time ./a.out real 0m0.808s time ./b.out real 0m0.566s struct.c : #include <stdlib.h> #include <stdio.h> #include "struct.h" int main() { int i,k; VecR *array; array=(VecR*)malloc(10000*sizeof(VecR)); #ifdef PERF for(i=0;i<10000;i++) array[i].cold=(ColdFields*)malloc(sizeof(ColdFields)); #endif for (i=0;i<10000;i++){ array[i].x = 1.0;array[i].y = 2.0;array[i].z = 0.0; } for(k=1;k<10000;k++) { for (i=k;i<9999;i++){ array[i].x = array[i-1].y+1.0; array[i].y = array[i+1].x+array[i+1].y; array[i].z = (array[i-1].y - array[i-1].x)/array[i-1].y; } } printf("%f \n",array[100].z); #ifdef PERF for(i=0;i<10000;i++) free(array[i].cold); #endif free(array); }
Излишние ссылки на память (Pointer chasing) • Доступ к данным через несколько ссылок – одна из самых распространенных проблем в С++ коде. Если структуры ваших данных не помещаются в подсистеме памяти, то при разыменовании каждой ссылки вы рискуете ожидать данные из памяти. Class Employers { Personal_info *p; … };; Class personal_info { Family_info *f; … }; Class family_info { int members; … }; All_members+= employer->p->f->members;