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

Министерство цифрового развития, связи и массовых коммуникаций РФ ордена Трудового Красного Знамени федеральное государственное бюджетное образовательное учреждение высшего образования «МОСКОВСКИЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ СВЯЗИ И ИНФОРМАТИКИ»

Кафедра «Системное программирование»

Лабораторная работа №9

«Исследование оптимизаций компилятора Clang»

по дисциплине «Системное программное обеспечение»

Выполнила:

студентка группы БФИ2203

Мячина Лада Андреевна

Проверила:

Старший преподаватель

Алексанян Диана Ашотовна

Москва, 2025

Цели работы:

 Изучить основные принципы оптимизации исходного кода

 Ознакомиться с влиянием оптимизаций компилятора Clang на промежуточное представление LLVM IR и результирующий файл на уровне инструкций ассемблера

 Приобрести практические навыки анализа промежуточного представления программы

Порядок выполнения работы

1. Для выполнение лабораторной работы допустимо как использование компилятора Clang, установленного в ОС, так и онлайн-компилятора godbolt.

При выполнении задания в ОС семейства Linux проверьте наличие компилятора командой clang --version и установите, при необходимости.

2. Ознакомьтесь с официальной документацией Clang, в том числе с опциями -S, emit-llvm, -O0, -O1, -O2.

3. Последовательно выполните анализ приведенного в заданиях кода с использованием опций компилятора -S, -emit-llvm, -O0, -O1, -O2 для получения требуемых результатов. Назначение опций, результаты работы и выводы включите в отчет.

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

Задания на лабораторную работу

Задание 1. Исследование базовых оптимизации

Проанализируйте приведенный код 1, сгенерируйте IR и ассемблер и включите в отчет

ответы на вопросы:

  1. Какие выражения были вычислены на этапе компиляции?

  2. Как изменилось количество инструкций?

  3. Какие типы оптимизаций были применены?

Листинг 1

int constant_operations() {

int a = 10 + 20 * 5;

int b = (1 << 4) | 0x0F;

float c = 3.14 * 2.0 + 1.0;

return a + b + (int)c;

}

Сгенерируйте LLVM IR и ассемблерный код для (листинг 2 и 3) с разными уровнями оптимизации. Проанализируйте полученные файлы и включите в отчет вывод о применении оптимизаций разного уровня.

Листинг 2

int dead_code_elimination() {

int x = 10;

int y = 20;

int z = x + y;

int unused1 = 100;

int unused2 = 200;

if (0) {

unused1 = 300;

}

return z;

}

Листинг 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;

}

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

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

Листинг 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;

}

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

Для приведенной «сложной» программы (листинг 5) определите, какие оптимизации были проведены при компиляции на различных уровнях оптимизации.

Листинг 4

#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;

}

Ход работы

Основные опции Clang

  • -S — не создавать исполняемый файл, а остановиться на этапе генерации ассемблерного кода (.s).

  • -emit-llvm — выводить LLVM IR (файлы .ll или .bc).

  • -O0 — без оптимизаций (debug-режим, максимально “прямой” код).

  • -O1, -O2 — оптимизирующие уровни.

    • -O1 — базовые оптимизации, умеренное ускорение.

    • -O2 — более агрессивные оптимизации (inlining, упрощение циклов, propagation, удаление мёртвого кода, loop-инварианты и т.д.).

Задание 1. Исследование базовых оптимизаций

Листинг 1 – Исходный код

Int constant_operations() {

int a = 10 + 20 * 5; // 10 + 100 = 110

int b = (1 << 4) | 0x0F; // 16 | 15 = 31

float c = 3.14 * 2.0 + 1.0; // приблизительно 7.28

return a + b + (int)c;

}

а) Что происходит при -O0

При уровне оптимизации -O0 компилятор почти не трогает исходную структуру кода:

  • в LLVM IR создаются инструкции mul, add, shl, or, fmul, fadd;

  • промежуточные переменные a, b, c обычно хранятся в памяти (alloca / store / load);

  • результат вычисляется по шагам.

Пример фрагмента IR в -O0:

%a = alloca i32

%b = alloca i32

%c = alloca float

; a = 10 + 20 * 5

store i32 10, i32* %a

; 20 * 5

%tmp1 = mul nsw i32 20, 5

%tmp2 = add nsw i32 10, %tmp1

; и т.д.

b) Что происходит при -O1 / -O2

При оптимизации Clang выполняет вычисление констант на этапе компиляции (constant folding):

  • 10 + 20 * 5 превращается в константу 110;

  • (1 << 4) | 0x0F превращается в константу 31;

  • 3.14 * 2.0 + 1.0 вычисляется в compile-time (в пределах точности IEEE-754), например 7.28f.

Соответственно, в IR уже нет эксплицитных арифметических операций, остаются только константы:

define i32 @constant_operations() {

entry:

; a = 110

; b = 31

; c ≈ 7.28

; return 110 + 31 + 7

ret i32 148

}

То есть функция целиком сворачивается в возврат одной константы (если компилятор считает допустимым такую оптимизацию).

Ответы на вопросы:

  1. Какие выражения были вычислены на этапе компиляции?

    • 20 * 5, затем 10 + (20*5);

    • (1 << 4) и далее (1 << 4) | 0x0F;

    • 3.14 * 2.0 + 1.0;

    • итоговое выражение a + b + (int)c также может быть вычислено на этапе компиляции, и функция превращается в return 148;.

  2. Как изменилось количество инструкций?

    • При -O0 в IR присутствуют:

      • отдельные инструкции для умножения, сложения, побитового сдвига и ИЛИ, операции с плавающей точкой;

      • операции выделения памяти (alloca) и загрузки/сохранения (load, store) для переменных a, b, c.

    • При -O2 и -О1 количество инструкций резко сокращается:

      • нет лишних alloca, все переменные заменены на константы;

      • в идеале остаётся фактически одна инструкция ret i32 <константа>.

  3. Какие типы оптимизаций были применены?

    • Constant folding (свёртка констант).

    • Constant propagation (распространение констант до места использования).

    • Elimination of dead code / unused temporaries — исчезают промежуточные переменные.

    • В IR также происходит scalar replacement of aggregates, удаление ненужных allocа.

Рисунок 1 – Сгенерированный ассемблер с оптимизатором -О0

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

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

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

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

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

Листинг 2 — dead_code_elimination

Int dead_code_elimination() {

int x = 10;

int y = 20;

int z = x + y;

int unused1 = 100;

int unused2 = 200;

if (0) {

unused1 = 300;

}

return z;

}

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

  • Все переменные (x, y, z, unused1, unused2) хранятся в стеке.

  • Условный оператор if (0) всё равно порождает базовые блоки в IR, хотя условие константно.

  • В IR видим:

    • присваивание unused1 и unused2;

    • ветку if (условный переход) с недостижимым блоком.

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

  • Компилятор видит, что условие if (0) никогда не выполняется, ветка мёртвая.

  • Переменные unused1 и unused2 нигде не используются.

Результат:

  • Условие if (0) полностью удаляется (dead code elimination).

  • Переменные unused1 и unused2 исчезают.

  • Функция сводится к:

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