Опубликован: 10.10.2011 | Доступ: свободный | Студентов: 1426 / 454 | Оценка: 4.31 / 4.16 | Длительность: 05:32:00
Специальности: Программист
Лекция 3:

Простейшие оптимизации программ

< Лекция 2 || Лекция 3: 123 || Лекция 4 >

Теперь обсудим 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;
    

Тест для оценки эффекта оптимизации loop unswitching:


Сравнение событий 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];
 }
    
< Лекция 2 || Лекция 3: 123 || Лекция 4 >