5dbe9ef165cfa948d30a418043cf142f.ppt
- Количество слайдов: 36
Простейшие оптимизации программ Ануфриенко Андрей Идрисов Ренат
Исходные файлы FE (C++/C или Fortran) Архитектура компилятора Внутреннее представление Временный файл или Obj с ВП Профилировщик Скалярные оптимизации IP/IPO оптимизации HPO Генератор кода Скалярные оптимизации HPO Генератор кода Обьектные файлы Исполняемый файл 10/17/10 Библиотека
Front End Синтаксический анализ (parsing) — это процесс анализа входной последовательности символов, с целью разбора грамматической структуры, обычно в соответствии с заданной формальной грамматикой. При этом исходный текст преобразуется в структуру данных, обычно — в дерево, которое отражает синтаксическую структуру входной последовательности и хорошо подходит для дальнейшей обработки. Обычно синтаксический анализ делится на два уровня: Лексический анализ — входной поток символов разбивается на линейную последовательность токенов — «слов» языка (напр. целые числа, идентификаторы, строковые константы и т. д. ); Семантический анализ — из токенов выделяются «предложения» языка, согласно грамматическим правилам, и создается дерево разбора На выходе FE мы получаем взаимосвязанные таблицы, которые называются внутренним представлением программы. Обычной практикой является использование общего внутреннего представления для разных языков высокого уровня. 10/17/10
Зависимости (Dependence) Вычисления являются эквивалентными, если на одинаковых данных они вычисляют одинаковые значения для выходных переменных и сохраняется порядок вывода результатов. Это определение позволяет использовать для вычисления различные последовательности инструкций (некоторые из которых могут быть более эффективными, чем другие). Какие особенности утверждений могут привести изменению результата в процессе вычисления?
Исходные файлы FE (C++/C или Fortran) Архитектура компилятора Внутреннее представление Временный файл или Obj с ВП Профилировщик Скалярные оптимизации IP/IPO оптимизации HPO Генератор кода Скалярные оптимизации HPO Генератор кода Обьектные файлы Исполняемый файл 10/17/10 Библиотека
Скалярные оптимизации Свертка констант, протяжка копий (Constant folding, constant propagation, copy propagation) Свертка констант - процесс вычисления констант во время компиляции. Протяжка констант – подстановка величин известных констант в выражение int x = 14; int y = 7 - x / 2; => constant propagation => int x = 14; int y = 7 – 14 / 2; Протяжка копий – процесс замены переменных их значениями y = x; z=3+y => copy propagation => z=3+x 10/17/10
Скалярные оптимизации Удаление повторных вычислений (Common subexpression elimination) – поиск идентичных подвыражений и сохранение результата вычисления во временной переменной для последующего повторного использования. a = b * c + g; d = b * c * d; => CSE => tmp = b * c; a = tmp + g; d = tmp * d; 10/17/10
Скалярные оптимизации Удаление мертвого кода (Dead code elimination) это удаление кода, который не изменяет выходных данных программы. К мертвому коду относится код, который никогда не выполняется или изменяет только не влияющие на результат переменные. int foo() { int a = 24; int b = 25; /* Присвоение не влияющей на результат переменной */ int c; c = a << 2; return c; b = 24; /* Недостижимый код */ } Мертвый код может появится после многих оптимизаций компилятора, после протяжки констант и копий, после прямой подстановки (inlining) и т. п. 10/17/10
Скалярные оптимизации Удаление излишнего ветвления, протяжка условий Удаляются блоки кода, которые не могут быть достижимы из-за цепочки условных ветвлений. if(x>0) { … if(x>0) { a=x; } } else { a=-x; } … } => if(x>0) { … a=x; … } Также может возникнуть из-за скалярных оптимизаций или прямой подстановки. 10/17/10
Анализ потоков данных (Data Flow Analysis) сбор информации о возможном наборе значений переменных вычисляемых в различных точках программы. Граф потока управления (CFG) используется для определения тех частей программы в которые может быть передано некоторое значение присвоенное переменной. Граф определения/использования (definition-use graph) – это граф, который содержит дуги из каждой точки определения переменной в программе к каждой точке ее использования. 10/17/10
SSA-форма SSA форма не позволяет создавать сложные цепочки зависимостей для переменных. Сила SSA заключается в том, что каждая переменная имеет только одно определение внутри программы. Поэтому любая зависимость очевидна. SSA представление вводит специальные Phi-функции в местах, в местах ветвления или условных операторов (например, if). Это так называемые псевдо-присваивания. При построении необходимо расставить Phi – функции и породить новые уникальные переменные. Новые переменные порождаются путем добавления к имени переменной уникального варианта. SSA-форма может использоваться не только разработчиком при создании программы, но и компилятором в процессе её оптимизации. Любая программа на императивном языке программирования может быть приведена к SSAформе 10/17/10
Исходные файлы FE (C++/C или Fortran) Архитектура компилятора Внутреннее представление Временный файл или Obj с ВП Профилировщик Скалярные оптимизации IP/IPO оптимизации HPO Генератор кода Скалярные оптимизации HPO Генератор кода Обьектные файлы Исполняемый файл 10/17/10 Библиотека
Циклы В большинстве случаев именно циклы являются «горячими местами» программы. Именно поэтому циклам уделяется много внимания как в архитектуре микропроцессора, так и в компиляторе. Loop Stream Detector – позволяет отказаться от выборки и декодирования инструкций для маленьких циклов. В компиляторе существует множество оптимизаций ориентированных именно на обработку циклов.
Распознание и классификация циклов Такие оптимизации, как правило, могут быть выполнены только для циклов • с определенным количеством итераций • имеющих последовательно изменяющиеся итерационные переменные, • не имеющих переходов за пределы цикла • не имеющих вызовов неизвестных функций Примеры «хороших» циклов 1. ) for(i=0; i=n) break; while(1);
Примеры «плохих» циклов: 1. ) for(i=0; i<3*i-n; i++) a[i]=i; 2. ) for(i=0; i
Обзор оптимизаций циклических конструкций Значительная часть оптимизаций компилятора связаны с циклами. Вынос инвариантов цикла (Loop invariant code motion) – оптимизация, которая находит и выносит за пределы цикла выражения, независящие от индексных переменных цикла. Т. е. это выражение неизменно на каждой итерации. while (j < maximum - 1) { j = j + (4+array[k])*pi+5; } => loop invariant code motion => int maxval = maximum - 1; int calcval = (4+array[k])*pi+5; while (j < maxval) { j = j + calcval; }
Вынос условных переходов (Loop unswitching) – оптимизация, которая выносит условные переходы инвариантные для цикла из цикла путем дублирования тела цикла. do i=1, 1000 x[i] = x[i] + y[i]; if (w) then y[i] = 0; end do; => loop unswitching => if (w) then do i=1, 1000 x[i] = x[i] + y[i]; y[i] = 0; end do; else do i=1, 1000 do x[i] = x[i] + y[i]; end do end if;
Тест для оценки эффекта оптимизации loop unswitching: #include
Сравнение событий BR_MISSP_EXEC для оригинального и модифицированного теста:
Привязка событий процессора к строкам кода:
Разбиение, объединение циклов (Loop distribution, loop fusion) – это обратные другу оптимизации. Компилятор должен иметь инструмент оценки выгодности таких оптимизаций. Разбиение циклов способно улучшить производительность за счет улучшения работы с памятью. Т. е. если цикл работает с большим количеством различных массивов, то может происходить вытеснение из кеша необходимых для последующих операций адресов. Из-за большого количества инвариантов цикла будет происходить вытеснение регистров (register spilling). Есть еще факторы, которые способны улучшить производительность при разбиении циклов. int i, a[100], b[100]; for (i = 0; i < 100; i++) { a[i] = 1; b[i] = 2; } => Loop distribution => for (i = 0; i < 100; i++) { a[i] = 1; } for (i = 0; i < 100; i++) { b[i] = 2; }
Объединение циклов может быть выгодным для небольших циклов за счет улучшения уровня инструкционного параллелизма и повторного использования данных. int i, a[100], b[100]; for (i = 0; i < 100; i++) { a[i] = 1; } for (i = 0; i < 100; i++) { b[i] = 2; } => Loop fusion for (i = 0; i < 100; i++) { a[i] = 1; b[i] = 2; } Различные микропроцессоры имеют различные критерии выгодности применения этой оптимизации.
Расщепление цикла (Loop peeling, splitting) – оптимизация, которая пытается упростить цикл «отщеплением» крайних итераций. p = 10; for (i=0; i<10; ++i) { y[i] = x[i] + x[p]; p = i; } Здесь p=10 только в первой итерации, а в дальнейшем p=i-1 =>Loop peeling => y[0] = x[0] + x[10]; for (i=1; i<10; ++i) { y[i] = x[i] + x[i-1]; }
Развертка цикла (Loop unrolling) – эта техника призвана уменьшить количество условных переходов в цикле. Может улучшить инструкционный параллелизм. Несколько итераций цикла объединяются в одну. Есть свои минусы. Может повыситься нагрузка на регистры, увеличивается размер кода, что также может привести к ухудшению производительности. for (int x = 0; x < 100; x++) { delete(x); } => Loop unrolling => for (int x = 0; x < 100; x += 5) { delete(x); delete(x+1); delete(x+2); delete(x+3); delete(x+4); } Полная развертка (complete unrolling) применяется к небольшим циклам и бывает очень эффективна. Как правило в случае вложенных циклов развертываются внутренние циклы.
Перестановка циклов (Loop interchange) – оптимизация при которой меняется порядок итерационных переменных. do i=0, 10 do j=0, 20 a[i, j] = i + j => Loop interchange do j=0, 20 do i=0, 10 a[i, j] = i + j ? Для чего выполняется эта оптимизация ?
INTEGER, PARAMETER : : N=100 INTEGER, PARAMETER : : REPEAT=1000 INTEGER : : A(N, N, N), B(N, N, N) INTEGER : : REP, I, J, K A=1 B=1 DO REP=1, REPEAT DO I=1, N DO J=1, N DO K=1, N A(J, K, I) = A(J, K, I)+B(J, K, I) END DO PRINT *, A(1, 1, 1) END Пример эффективности оптимизации перестановки циклов: ifort -O 3 test. f 90 -o a. out ifort -O 2 test. f 90 -o b. out Сообщение выдаваемое компилятором ( с внутренними ключом) сообщает, что в первом случае компилятор совершил перестановку циклов. … LOOP INTERCHANGE in loops at line: 9 11 12 13 Loopnest permutation ( 1 2 3 4 ) --> ( 2 4 1 3 ) … time. /a. out real 0 m 0. 960 s time. /b. out real 0 m 6. 862 s
Вытеснение данных из кэш:
INTEGER, PARAMETER : : N=2000 INTEGER : : BF, BN, I, J, K, I 1, J 1, K 1 DOUBLE PRECISION, ALLOCATABLE : : A(: , : ), B(: , : ), C(: , : ) ALLOCATE(A(N, N), B(N, N), C(N, N)) A=1 B=-1 #ifdef PERF BF=8 BN=N/BF DO I 1=1, BF DO J 1=1, BF DO K 1=1, BF DO I=1+BN*(I 1 -1), MIN(BN*I 1, N) DO J=1+BN*(J 1 -1), MIN(BN*J 1, N) DO K=1+BN*(K 1 -1), MIN(BN*K 1, N) C(J, I) = C(J, I) + A(I, K)*B(K, J) END DO END DO #else DO I=1, N DO J=1, N DO K=1, N C(J, I) = C(J, I) + A(I, K)*B(K, J) END DO #endif PRINT *, C(1: 100, 700: 800) END Разбиение цикла на блоки (Loop blocking) ifort loop_blocking. F 90 /fpp –Od -Feoriginal ifort loop_blocking. F 90 /fpp –DPERF –Od –Feblocking Nehalem: Time original. exe ~150 s Time blocking. exe ~105 s
10/17/10
10/17/10
Определение индукционных переменных Выражения, которые являются линейной функцией от счетчиков цикла могут вычисляться прибавлением константы к значению на предыдущей итерации. for(i=0; i<100; i++) { a[i]=i*8+4; } => temp=4; for(i=0; i<100; i++) { a[i] = temp; temp +=8; } 10/17/10
Другие оптимизации Помимо этих оптимизаций существуют и другие, порой очень нетривиальные: Strength reduction Scalar expansion Loop skewing Loop coalescing Loop collapsing и многие другие. Компилятору в каждом случае необходимо доказать корректность производимой цикловой оптимизации и определить ее выгодность.
Нормализованный цикл Предположим у нас есть цикл: DO I=L, U, S A(I)=… END DO Для упрощения рассуждений его можно преобразовать к нормализованному циклу: DO J=1, (U-L+S)/S, 1 A(J*S-S+L) = … END DO Так как любой цикл может быть нормализован будем без ограничения общности рассматривать в примерах нормализованные циклы.
/Qopt-report[: n] generate an optimization report to stderr 0 disable optimization report output 1 minimum report output 2 medium output (DEFAULT when enabled) 3 maximum report output Пример диагностики: LOOP INTERCHANGE in loops at line: 8 9 Loopnest permutation ( 1 2 ) --> ( 2 1 ) Fusion loop partitions: (loop line numbers) Fused Loops: ( 9 14 ) 10/17/10
Спасибо за внимание!


