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

Начнем разговор о архитектуре компилятора с разговора о
Front End
Синтаксический анализ (parsing) — это процесс анализа входной последовательности символов, с целью разбора грамматической структуры, обычно в соответствии с заданной
При этом исходный текст преобразуется в структуру данных, обычно — в дерево, которое отражает синтаксическую структуру входной последовательности и хорошо подходит для дальнейшей обработки.
Обычно синтаксический анализ делится на два уровня:
На выходе
В современном оптимизирующем компиляторе фронт-енды составляют малую часть от всей функциональности компилятора. В данном случае разные языковые конструкции (С,С++,Fortran) переводятся в общее внутреннее представление.
Зависимости (Dependence)
Вычисления являются эквивалентными, если на одинаковых данных они вычисляют одинаковые значения для выходных переменных и сохраняется порядок вывода результатов.
Это определение позволяет использовать для вычисления различные последовательности инструкций (некоторые из которых могут быть более эффективными, чем другие).
Какие особенности утверждений могут привести к изменению результата в процессе вычисления?
Мы рассмотрели примеры некоторых базовых цикловых оптимизаций. Чтобы понять область их применения обсудим зависимости и что подразумевается под допустимыми оптимизациями.

Скалярные оптимизации
Свертка констант, протяжка констант, протяжка копий (
Свертка констант - процесс вычисления констант во время компиляции.
Протяжка констант – подстановка величин известных констант в выражение
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
a = b * c + g;
d = b * c * d;
=> CSE =>
tmp = b * c;
a = tmp + g;
d = tmp * d;
Скалярные оптимизации
Удаление мертвого кода (
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)
Сбор информации о возможном наборе значений переменных вычисляемых в различных точках программы. Граф потока управления (
Граф определения/использования (definition-use
Скалярные оптимизации базируются на
SSA-форма
if). Это так называемые псевдо-присваивания.
При построении необходимо расставить Phi – функции и породить новые уникальные переменные.
Новые переменные порождаются путем добавления к имени переменной уникального варианта.
Любая программа на
![]() |
![]() |

Теперь обсудим HPO (high
Циклы
В большинстве случаев именно циклы являются "горячими местами" программы.
Именно поэтому циклам уделяется много внимания как в архитектуре микропроцессора, так и в компиляторе.
Loop Stream
В компиляторе существует множество оптимизаций ориентированных именно на обработку циклов.
Распознание и классификация циклов
Такие оптимизации, как правило, могут быть выполнены только для циклов
Примеры "хороших" циклов
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); }
При программировании учитывайте сложность используемых конструкций. Избегайте циклов с неопределенным количеством итераций.
При написании программы нужно учитывать, что циклы не всегда могут быть распознаны компилятором.
Обзор оптимизаций циклических конструкций
Значительная часть оптимизаций компилятора связаны с циклами.
Вынос инвариантов цикла (
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;
}
Вынос условных переходов (
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) – это обратные друг другу оптимизации. Компилятор должен иметь инструмент оценки выгодности таких оптимизаций.
Разбиение циклов способно улучшить производительность за счет улучшения работы с памятью. Т.е. если цикл работает с большим количеством различных массивов, то может происходить вытеснение из
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;
}
Различные микропроцессоры имеют различные критерии выгодности применения этой оптимизации.
Расщепление цикла (
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];
}
Развертка цикла (
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) применяется к небольшим циклам и бывает очень эффективна. Как правило в случае вложенных циклов развертываются внутренние циклы.
Перестановка циклов (
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;
}
Другие оптимизации
Помимо этих оптимизаций существуют и другие, порой очень нетривиальные:
| Loop |
| Loop coalescing |
Компилятору в каждом случае необходимо доказать корректность производимой цикловой оптимизации и определить ее выгодность.
Нормализованный цикл
Предположим у нас есть цикл:
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, которая частично информирует пользователя о произведенных преобразованиях.
Презентацию к лекции Вы можете скачать здесь.

Начнем разговор о архитектуре компилятора с разговора о
Front End
Синтаксический анализ (parsing) — это процесс анализа входной последовательности символов, с целью разбора грамматической структуры, обычно в соответствии с заданной
При этом исходный текст преобразуется в структуру данных, обычно — в дерево, которое отражает синтаксическую структуру входной последовательности и хорошо подходит для дальнейшей обработки.
Обычно синтаксический анализ делится на два уровня:
На выходе
В современном оптимизирующем компиляторе фронт-енды составляют малую часть от всей функциональности компилятора. В данном случае разные языковые конструкции (С,С++,Fortran) переводятся в общее внутреннее представление.
Зависимости (Dependence)
Вычисления являются эквивалентными, если на одинаковых данных они вычисляют одинаковые значения для выходных переменных и сохраняется порядок вывода результатов.
Это определение позволяет использовать для вычисления различные последовательности инструкций (некоторые из которых могут быть более эффективными, чем другие).
Какие особенности утверждений могут привести к изменению результата в процессе вычисления?
Мы рассмотрели примеры некоторых базовых цикловых оптимизаций. Чтобы понять область их применения обсудим зависимости и что подразумевается под допустимыми оптимизациями.

Скалярные оптимизации
Свертка констант, протяжка констант, протяжка копий (
Свертка констант - процесс вычисления констант во время компиляции.
Протяжка констант – подстановка величин известных констант в выражение
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
a = b * c + g;
d = b * c * d;
=> CSE =>
tmp = b * c;
a = tmp + g;
d = tmp * d;
Скалярные оптимизации
Удаление мертвого кода (
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)
Сбор информации о возможном наборе значений переменных вычисляемых в различных точках программы. Граф потока управления (
Граф определения/использования (definition-use
Скалярные оптимизации базируются на
SSA-форма
if). Это так называемые псевдо-присваивания.
При построении необходимо расставить Phi – функции и породить новые уникальные переменные.
Новые переменные порождаются путем добавления к имени переменной уникального варианта.
Любая программа на
![]() |
![]() |

Теперь обсудим HPO (high
Циклы
В большинстве случаев именно циклы являются "горячими местами" программы.
Именно поэтому циклам уделяется много внимания как в архитектуре микропроцессора, так и в компиляторе.
Loop Stream
В компиляторе существует множество оптимизаций ориентированных именно на обработку циклов.
Распознание и классификация циклов
Такие оптимизации, как правило, могут быть выполнены только для циклов
Примеры "хороших" циклов
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); }
При программировании учитывайте сложность используемых конструкций. Избегайте циклов с неопределенным количеством итераций.
При написании программы нужно учитывать, что циклы не всегда могут быть распознаны компилятором.
Обзор оптимизаций циклических конструкций
Значительная часть оптимизаций компилятора связаны с циклами.
Вынос инвариантов цикла (
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;
}
Вынос условных переходов (
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) – это обратные друг другу оптимизации. Компилятор должен иметь инструмент оценки выгодности таких оптимизаций.
Разбиение циклов способно улучшить производительность за счет улучшения работы с памятью. Т.е. если цикл работает с большим количеством различных массивов, то может происходить вытеснение из
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;
}
Различные микропроцессоры имеют различные критерии выгодности применения этой оптимизации.
Расщепление цикла (
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];
}
Развертка цикла (
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) применяется к небольшим циклам и бывает очень эффективна. Как правило в случае вложенных циклов развертываются внутренние циклы.
Перестановка циклов (
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;
}
Другие оптимизации
Помимо этих оптимизаций существуют и другие, порой очень нетривиальные:
| Loop |
| Loop coalescing |
Компилятору в каждом случае необходимо доказать корректность производимой цикловой оптимизации и определить ее выгодность.
Нормализованный цикл
Предположим у нас есть цикл:
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, которая частично информирует пользователя о произведенных преобразованиях.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.