Введение в оптимизацию приложений с использованием компиляторов Intel

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

Разбить на страницы
Показывать лекцию целиком

Презентацию к лекции Вы можете скачать здесь.

Начнем разговор о архитектуре компилятора с разговора о FE и о внутреннем представлении компилируемой программы.

Front End

Синтаксический анализ (parsing) — это процесс анализа входной последовательности символов, с целью разбора грамматической структуры, обычно в соответствии с заданной формальной грамматикой.

При этом исходный текст преобразуется в структуру данных, обычно — в дерево, которое отражает синтаксическую структуру входной последовательности и хорошо подходит для дальнейшей обработки.

Обычно синтаксический анализ делится на два уровня:

  • Лексический анализ — входной поток символов разбивается на линейную последовательность токенов — "слов" языка (напр. целые числа, идентификаторы, строковые константы и т. д.);
  • Семантический анализ — из токенов выделяются "предложения" языка, согласно грамматическим правилам, и создается дерево разбора
  • На выходе FE мы получаем взаимосвязанные таблицы, которые называются внутренним представлением программы. Обычной практикой является использование общего внутреннего представления для разных языков высокого уровня

    В современном оптимизирующем компиляторе фронт-енды составляют малую часть от всей функциональности компилятора. В данном случае разные языковые конструкции (С,С++,Fortran) переводятся в общее внутреннее представление.

    Зависимости (Dependence)

    Вычисления являются эквивалентными, если на одинаковых данных они вычисляют одинаковые значения для выходных переменных и сохраняется порядок вывода результатов.

    Это определение позволяет использовать для вычисления различные последовательности инструкций (некоторые из которых могут быть более эффективными, чем другие).

    Какие особенности утверждений могут привести к изменению результата в процессе вычисления?

    Мы рассмотрели примеры некоторых базовых цикловых оптимизаций. Чтобы понять область их применения обсудим зависимости и что подразумевается под допустимыми оптимизациями.

    Скалярные оптимизации

    Свертка констант, протяжка констант, протяжка копий (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
        

    Скалярные оптимизации

    Удаление повторных вычислений (Common subexpression elimination) – поиск идентичных подвыражений и сохранение результата вычисления во временной переменной для последующего повторного использования.

    a = b * c + g; 
    d = b * c * d; 
     => CSE =>
    tmp = b * c; 
    a = tmp + g;
    d = tmp * d; 
        

    Скалярные оптимизации

    Удаление мертвого кода (Dead code elimination) это удаление кода, который не изменяет выходных данных программы. К мертвому коду относится код, который никогда не выполняется или изменяет только не влияющие на результат переменные.

    int foo() { 
    int a = 24; 
    int b = 25; /* Присвоение не влияющей на результат переменной */ 
    int c; 
    c = a << 2; 
    return c; 
    b = 24; /* Недостижимый код */ } 
        

    Мертвый код может появиться после многих оптимизаций компилятора, после протяжки констант и копий, после прямой подстановки (inlining) и т.п.

    Скалярные оптимизации

    Удаление излишнего ветвления, протяжка условий

    Удаляются блоки кода, которые не могут быть достижимы из-за цепочки условных ветвлений.

     if(x>0) {
     …
        if(x>0) {      a=x;    }
        else  {      a=-x;   } 
     …
    }  
    =>
    if(x>0) {
    …
    a=x;
    …
    }
        

    Также может возникнуть из-за скалярных оптимизаций или прямой подстановки.

    Анализ потоков данных (Data Flow Analysis)

    Сбор информации о возможном наборе значений переменных вычисляемых в различных точках программы. Граф потока управления (CFG) используется для определения тех частей программы, в которые может быть передано некоторое значение, присвоенное переменной.

    Граф определения/использования (definition-use graph) – это граф, который содержит дуги из каждой точки определения переменной в программе к каждой точке ее использования.

    Скалярные оптимизации базируются на анализе потоков данных. Анализ потоков данных работает с графом потока управления. Действительно, все сложности с анализом данных появляются из-за различных ветвлений управления, циклов и переходов. В непрерывном коде (именно непрерывные части кода и являются телами базовых блоков CFG) задача анализа потока данных очень проста.

    SSA-форма

    SSA форма не позволяет создавать сложные цепочки зависимостей для переменных. Сила SSA заключается в том, что каждая переменная имеет только одно определение внутри программы. Поэтому любая зависимость очевидна. SSA представление вводит специальные Phi-функции в местах, в местах ветвления или условных операторов (например, if). Это так называемые псевдо-присваивания.

    При построении необходимо расставить Phi – функции и породить новые уникальные переменные.

    Новые переменные порождаются путем добавления к имени переменной уникального варианта.

    SSA-форма может использоваться не только разработчиком при создании программы, но и компилятором в процессе её оптимизации.

    Любая программа на императивном языке программирования может быть приведена к SSA-форме

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

    Развертка цикла (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
        

    Для чего выполняется эта оптимизация ?

    Пример эффективности оптимизации перестановки циклов

    Вытеснение данных из кэш:

    Разбиение цикла на блоки (Loop blocking)

    Определение индукционных переменных

    Выражения, которые являются линейной функцией от счетчиков цикла могут вычисляться прибавлением константы к значению на предыдущей итерации.

      for(i=0;i<100;i++) {
        a[i]=i*8+4;
      } 
      =>
      temp=4;
      for(i=0;i<100;i++) {
        a[i] = temp;
        temp +=8;
      }
        

    Другие оптимизации

    Помимо этих оптимизаций существуют и другие, порой очень нетривиальные:

    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 )
        

    Компилятор не ставит пользователя в известность, какие цикловые оптимизации и в каком порядке принимаются. Есть опция /Qopt-report, которая частично информирует пользователя о произведенных преобразованиях.

    Страницы:

    Презентацию к лекции Вы можете скачать здесь.

    Начнем разговор о архитектуре компилятора с разговора о FE и о внутреннем представлении компилируемой программы.

    Front End

    Синтаксический анализ (parsing) — это процесс анализа входной последовательности символов, с целью разбора грамматической структуры, обычно в соответствии с заданной формальной грамматикой.

    При этом исходный текст преобразуется в структуру данных, обычно — в дерево, которое отражает синтаксическую структуру входной последовательности и хорошо подходит для дальнейшей обработки.

    Обычно синтаксический анализ делится на два уровня:

  • Лексический анализ — входной поток символов разбивается на линейную последовательность токенов — "слов" языка (напр. целые числа, идентификаторы, строковые константы и т. д.);
  • Семантический анализ — из токенов выделяются "предложения" языка, согласно грамматическим правилам, и создается дерево разбора
  • На выходе FE мы получаем взаимосвязанные таблицы, которые называются внутренним представлением программы. Обычной практикой является использование общего внутреннего представления для разных языков высокого уровня

    В современном оптимизирующем компиляторе фронт-енды составляют малую часть от всей функциональности компилятора. В данном случае разные языковые конструкции (С,С++,Fortran) переводятся в общее внутреннее представление.

    Зависимости (Dependence)

    Вычисления являются эквивалентными, если на одинаковых данных они вычисляют одинаковые значения для выходных переменных и сохраняется порядок вывода результатов.

    Это определение позволяет использовать для вычисления различные последовательности инструкций (некоторые из которых могут быть более эффективными, чем другие).

    Какие особенности утверждений могут привести к изменению результата в процессе вычисления?

    Мы рассмотрели примеры некоторых базовых цикловых оптимизаций. Чтобы понять область их применения обсудим зависимости и что подразумевается под допустимыми оптимизациями.

    Скалярные оптимизации

    Свертка констант, протяжка констант, протяжка копий (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
        

    Скалярные оптимизации

    Удаление повторных вычислений (Common subexpression elimination) – поиск идентичных подвыражений и сохранение результата вычисления во временной переменной для последующего повторного использования.

    a = b * c + g; 
    d = b * c * d; 
     => CSE =>
    tmp = b * c; 
    a = tmp + g;
    d = tmp * d; 
        

    Скалярные оптимизации

    Удаление мертвого кода (Dead code elimination) это удаление кода, который не изменяет выходных данных программы. К мертвому коду относится код, который никогда не выполняется или изменяет только не влияющие на результат переменные.

    int foo() { 
    int a = 24; 
    int b = 25; /* Присвоение не влияющей на результат переменной */ 
    int c; 
    c = a << 2; 
    return c; 
    b = 24; /* Недостижимый код */ } 
        

    Мертвый код может появиться после многих оптимизаций компилятора, после протяжки констант и копий, после прямой подстановки (inlining) и т.п.

    Скалярные оптимизации

    Удаление излишнего ветвления, протяжка условий

    Удаляются блоки кода, которые не могут быть достижимы из-за цепочки условных ветвлений.

     if(x>0) {
     …
        if(x>0) {      a=x;    }
        else  {      a=-x;   } 
     …
    }  
    =>
    if(x>0) {
    …
    a=x;
    …
    }
        

    Также может возникнуть из-за скалярных оптимизаций или прямой подстановки.

    Анализ потоков данных (Data Flow Analysis)

    Сбор информации о возможном наборе значений переменных вычисляемых в различных точках программы. Граф потока управления (CFG) используется для определения тех частей программы, в которые может быть передано некоторое значение, присвоенное переменной.

    Граф определения/использования (definition-use graph) – это граф, который содержит дуги из каждой точки определения переменной в программе к каждой точке ее использования.

    Скалярные оптимизации базируются на анализе потоков данных. Анализ потоков данных работает с графом потока управления. Действительно, все сложности с анализом данных появляются из-за различных ветвлений управления, циклов и переходов. В непрерывном коде (именно непрерывные части кода и являются телами базовых блоков CFG) задача анализа потока данных очень проста.

    SSA-форма

    SSA форма не позволяет создавать сложные цепочки зависимостей для переменных. Сила SSA заключается в том, что каждая переменная имеет только одно определение внутри программы. Поэтому любая зависимость очевидна. SSA представление вводит специальные Phi-функции в местах, в местах ветвления или условных операторов (например, if). Это так называемые псевдо-присваивания.

    При построении необходимо расставить Phi – функции и породить новые уникальные переменные.

    Новые переменные порождаются путем добавления к имени переменной уникального варианта.

    SSA-форма может использоваться не только разработчиком при создании программы, но и компилятором в процессе её оптимизации.

    Любая программа на императивном языке программирования может быть приведена к SSA-форме

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

    Развертка цикла (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
        

    Для чего выполняется эта оптимизация ?

    Пример эффективности оптимизации перестановки циклов

    Вытеснение данных из кэш:

    Разбиение цикла на блоки (Loop blocking)

    Определение индукционных переменных

    Выражения, которые являются линейной функцией от счетчиков цикла могут вычисляться прибавлением константы к значению на предыдущей итерации.

      for(i=0;i<100;i++) {
        a[i]=i*8+4;
      } 
      =>
      temp=4;
      for(i=0;i<100;i++) {
        a[i] = temp;
        temp +=8;
      }
        

    Другие оптимизации

    Помимо этих оптимизаций существуют и другие, порой очень нетривиальные:

    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 )
        

    Компилятор не ставит пользователя в известность, какие цикловые оптимизации и в каком порядке принимаются. Есть опция /Qopt-report, которая частично информирует пользователя о произведенных преобразованиях.

    Вернуться к учебному плану