Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЛР9.docx
Скачиваний:
0
Добавлен:
02.08.2026
Размер:
2 Мб
Скачать

Int dead_code_elimination() {

return 30; // 10 + 20

}

Соответствующий IR:

define i32 @dead_code_elimination() {

entry:

ret i32 30

}

Типы оптимизаций:

  • Dead code elimination — удаление неиспользуемых переменных и недостижимых веток.

  • Constant folding / propagation — свёртка x + y в константу 30.

  • Упрощение CFG (Control Flow Graph): удаление лишних базовых блоков.

Рисунок 7 - Сгенерированный ассемблер с оптимизатором -O0

Рисунок 8 - Сгенерированный LLVM IR с оптимизатором -O0

Рисунок 9 - Сгенерированный ассемблер с оптимизатором -O2

Рисунок 10 - Сгенерированный LLVM IR с оптимизатором -O2

Листинг 3 — Инлайнинг функций

Исходный код:

Int helper1(int a, int b) {

return a * b + 10;

}

Int helper2(int X) {

return helper1(x, x + 1);

}

Int main_inline() {

int result = 0;

for (int i = 0; i < 10; i++) {

result += helper2(i);

}

return result;

}

Поведение при -O0

  • Все функции отдельные, без инлайнинга.

  • В main_inline на каждой итерации цикла:

    • вызывается helper2(i);

    • внутри неё вызывается helper1(x, x + 1).

  • В IR видно несколько функций, вызовы call.

Поведение при -O1 / -O2

На оптимизирующих уровнях компилятор обычно выполняет:

  1. Function inlining:

    • Тело helper1 подставляется внутрь helper2.

    • Затем helper2 может быть также заинлайнена в main_inline, особенно при -O2.

После полного инлайнинга в main_inline:

Int main_inline() {

int result = 0;

for (int i = 0; i < 10; i++) {

// result += helper2(i);

// helper2(i) = helper1(i, i+1) = i*(i+1) + 10

result += i * (i + 1) + 10;

}

return result;

}

Далее возможно:

  1. Strength reduction / constant folding внутри цикла (частично).

Типы оптимизаций:

  • Inlining (встраивание функций) — уменьшение overhead вызовов.

  • Loop-invariant optimizations (если появляются инварианты).

  • Constant folding внутри цикла и, потенциально, полная свёртка цикла (loop unrolling + constant propagation).

  • Уменьшение количества функций и вызовов в ассемблере: меньше инструкций call.

Рисунок 11 - Сгенерированный ассемблер с оптимизатором -O0

Рисунок 12 - Сгенерированный LLVM IR с оптимизатором -O0

Рисунок 13 - Сгенерированный ассемблер с оптимизатором -O2

Рисунок 14 - Сгенерированный LLVM IR с оптимизатором -O2

Задание 2. Оптимизация циклов

Листинг 4:

// Простой цикл с инвариантом

int simple_loop() {

int sum = 0;

for (int i = 0; i < 100; i++) {

sum += i;

}

return sum;

}

// Цикл с инвариантными вычислениями

int loop_with_invariant(int n) {

int result = 0;

int constant = 100 * 50; // инвариант

for (int i = 0; i < n; i++) {

result += i + constant;

}

return result;

}

// Вложенные циклы

int nested_loops() {

int sum = 0;

for (int i = 0; i < 10; i++) {

for (int j = 0; j < 10; j++) {

sum += i * j;

}

}

return sum;

}

4.2.1. simple_loop

  • При -O0: обычный цикл, много инструкций:

    • инициализация i = 0;

    • сравнение i < 100;

    • переход назад;

    • sum += i.

  • При -O1/-O2:

    • Компилятор видит, что цикл полностью детерминирован: сумма от 0 до 99.

    • Выполняется loop unrolling (частичный) или вообще полная свёртка: сумма вычисляется при компиляции как 4950.

    • Итоговая функция:

    • int simple_loop() {

    • return 4950;

    • }

    • В IR/asm остаётся одна константа.

Оптимизации:

  • Constant folding / propagation.

  • Loop unrolling / loop elimination (цикл полностью удалён).

  • Dead code elimination (удалены все счётчики).

4.2.2. loop_with_invariant

Здесь constant = 100 * 50:

  • 100 * 50 — константное выражение. При оптимизации:

    • до входа в цикл вычисляется константа 5000;

    • значение constant рассматривается как loop-invariant.

  • Внутри цикла остаётся:

  • result += i + 5000;

При -O2:

  • часто выполняются loop-invariant code motion (LICM) — хотя в исходнике constant уже вне цикла, компилятор может дополнительно упростить вычисления.

  • возможны оптимизации для случая, когда n известно (если вызывать с фиксированным n и включить LTO, можно свернуть цикл полностью).

Оптимизации:

  • Constant folding: 100 * 50 → 5000.

  • Loop-invariant code usage: переменная constant не пересчитывается в цикле, используется как константный регистр.

  • Возможное частичное развёртывание цикла (unrolling) для производительности.

4.2.3. nested_loops

  • При -O0: два вложенных цикла, явные i, j, умножение i * j в теле.

  • При -O1 / -O2:

    • i и j становятся индукционными переменными, хранятся в регистрах.

    • Может выполняться strength reduction — вместо того, чтобы каждый раз вычислять i * j через инструкцию mul, компилятор может преобразовать часть выражений к цепочке сложений, но современный компилятор часто оставляет mul, т.к. оно дешёвое.

    • Возможен loop unrolling внешнего и/или внутреннего циклов (особенно при малых границах 0..9).

    • Возможна полная свёртка (так как пределы известны), и вычисление суммы ∑i∑j i*j во время компиляции, в результате функция вернёт константу.

Оптимизации:

  • Strength reduction (частично).

  • Loop unrolling.

  • Возможная полная свёртка вложенных циклов.

  • Оптимизация использования регистров и индексации.

Рисунок 15 - Сгенерированный ассемблер с оптимизатором -O0

Рисунок 16 - Продолжение сгенерированного ассемблера с оптимизатором -O0

Рисунок 17 - Сгенерированный LLVM IR с оптимизатором -O0

Рисунок 18 - Продолжение сгенерированного LLVM IR с оптимизатором -O0

Рисунок 19 - Сгенерированный LLVM IR с оптимизатором -O1

Рисунок 20 - Сгенерированный ассемблер с оптимизатором -O2

Рисунок 21 - Сгенерированный LLVM IR с оптимизатором -O2

Задание 3. Анализ оптимизаций “сложной” программы

Исходный код:

#include <stdio.h>

int calculate(int base, int iterations) {

int result = base;

if (iterations < 0) {

printf("This should never happen\n");

return -1;

}

for (int i = 0; i < iterations; i++) {

if (i % 2 == 0) {

result += i * 2;

} else {

result += i;

}

int invariant = 100 * 50;

result += invariant;

}

int final_adjustment = (10 + 20) * 3;

result += final_adjustment;

return result;

}

int main() {

int value = calculate(10, 5);

printf("Result: %d\n", value);

return 0;

}

4.3.1. Что делает компилятор при разных уровнях оптимизации

При -O0

  • Сохраняется структура программы:

    • условие if (iterations < 0) реализовано ветвлением;

    • цикл for с явным счётчиком;

    • внутри цикла вычисляется i % 2 — реальная операция деления (или специальные инструкции);

    • int invariant = 100 * 50; вычисляется на каждой итерации;

    • после цикла — вычисление final_adjustment = (10 + 20) * 3; как обычные операции.

  • В main:

    • вызывается calculate(10, 5);

    • затем printf.

При -O1 / -O2

1) Свёртка констант и инвариантов

  • int invariant = 100 * 50;→ constant folding → int invariant = 5000;.

  • int final_adjustment = (10 + 20) * 3;→ constant folding → int final_adjustment = 90;.

2) Loop-invariant code motion

Переменная invariant объявлена внутри цикла, но её значение не зависит от i:

for (...) {

int invariant = 5000;

result += invariant;

}

Оптимизированный вариант:

int invariant = 5000;

for (...) {

result += invariant;

}

То есть инвариант выносится из тела цикла, чтобы не пересчитывать на каждой итерации.

3) Оптимизация проверки чётности

Выражение:

if (i % 2 == 0)

  • При оптимизации % 2 превращается в более дешёвую операцию:

if ((i & 1) == 0)

То есть деление заменено на побитовое И (strength reduction).

4) Упрощение ветвления внутри цикла

Внутри цикла:

if (i % 2 == 0) {

result += i * 2;

} else {

result += i;

}

Может быть переписано компилятором в форму без явного ветвления (branchless), например с использованием условных инструкций/селекторов:

int add_even = i * 2;

int add_odd = i;

int to_add = (i & 1) ? add_odd : add_even;

result += to_add;

В LLVM IR это может быть через инструкцию select.

5) Вызовы printf и условная ветка iterations < 0

  • Параметр iterations — аргумент функции, его значение не известно на этапе компиляции (если не включать whole-program оптимизации и не анализировать конкретный вызов).

  • Поэтому ветка if (iterations < 0) не удаляется, но:

    • Проверка упрощается (нет лишних копий, используется регистр).

    • Код в недостижимой ветке (после return -1) не оптимизируется сильно, но может быть слегка упорядочен.

Соседние файлы в предмете Системное программное обеспечение