Простейшие оптимизации программ
Теперь обсудим HPO (high performance optimizations).
Циклы
В большинстве случаев именно циклы являются "горячими местами" программы.
Именно поэтому циклам уделяется много внимания как в архитектуре микропроцессора, так и в компиляторе.
Loop Stream Detector – позволяет отказаться от выборки и декодирования инструкций для маленьких циклов.
В компиляторе существует множество оптимизаций ориентированных именно на обработку циклов.
Распознание и классификация циклов
Такие оптимизации, как правило, могут быть выполнены только для циклов
- с определенным количеством итераций
- имеющих последовательно изменяющиеся итерационные переменные
- не имеющих переходов за пределы цикла
- не имеющих вызовов неизвестных функций
Примеры "хороших" циклов
1.) for(i=0;i<U;i++) a[i]=b[i]; 2.) i=0; do { a[i]=b[i]; i++; } while(i<U); 3.) for(i=0;i<U;i++) { a[j]=b[i]; j+=c*i; } 4.) i=0; do { a[i]=b[i]; if(i++>=n) break; while(1);
В общем случае компилятор не информирует пользователя о том, распознал ли компилятор пользовательские циклы. Т.е. если программист использовал конструкции while или for, то с точки зрения компилятора это еще не значит, что в программе существуют циклы.
Примеры "плохих" циклов:
1.) for(i=0;i<3*i-n;i++) a[i]=i; 2.) for(i=0;i<n;i++) { a[i]=i; if(i<t) break; } 3.) for(i=0;i<n;i++) { a[i]=i; if(i==t) goto loop_skip; } 4.) for(i=0;i<n;i++) { a[i]=i; t=g(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;
Сравнение событий BR_MISSP_EXEC для оригинального и модифицированного теста:
Привязка событий процессора к строкам кода:
Разбиение, объединение циклов (Loop distribution, loop fusion) – это обратные друг другу оптимизации. Компилятор должен иметь инструмент оценки выгодности таких оптимизаций.
Разбиение циклов способно улучшить производительность за счет улучшения работы с памятью. Т.е. если цикл работает с большим количеством различных массивов, то может происходить вытеснение из кеша необходимых для последующих операций адресов. Из-за большого количества инвариантов цикла будет происходить вытеснение регистров (register spilling). Есть еще факторы, которые способны улучшить производительность при разбиении циклов.
int i, a[100], b[100]; 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]; 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]; }