Глава 2. Автоматични оптимизационни преобразования

Съдържание

Локална оптимизация
Глобална оптимизация
Проблеми
Модификаторите const и restrict
Междупроцедурна оптимизация

Съвременните компилатори са много „по-умни“, отколкото обикновено предполагаме. Те избавят програмиста от много досадна работа. Само преди 6–7 години това не беше така. Интелигентността на днешните компилатори позволява да пишем четливи и прегледни програми, без това да се отразява на ефективността. Но за да умеем да се възползваме от уменията на компилаторите, ние трябва да ги познаваме. Езикът Си е архаичен, от ниско ниво и подтиква програмистите сами да оптимизират програмите си. В действителност често тези опити само затрудняват компилаторите и влияят отрицателно на ефективността.

Оптимизационните преобразования, извършвани от съвременните компилатори можем да разделим на локална, глобална и междупроцедурна оптимизация.

Локална оптимизация

Локалната оптимизация се осъществявя в рамките на линеен участък от програмата — без условни оператори, без цикли. Основната задача тук е да се разпределят променливите и междинните резултати от изчисленията по оптимален начин между регистрите на процесора и оперативната памет. Всъщност в оптимизираната програма променливите съществуват само условно. Една и съща променлива може веднъж да се намира в един регистър, друг път в друг, трети път да бъде в оперативната памет (напр. стека), а понякога може и изобщо да не съществува, ако компилаторът е преценил, че стойността й няма да потрябва. Регистрите съдържат това, което е най-потребно; понякога това могат да бъдат дори масиви!

Друг вид локална оптимицация е в премахването на многократни изчисления на едно и също нещо. Ако един и същи израз се среща многократно, стойността му ще бъде изчислена само веднъж.

Пример 2.1. Обединяване на общи операции

Няма нужда да опростяваме следната последователност от оператори:

a [i] = (i+1)/2 + 1;
b [i] = (i+1)/2 + 2;
          

Компилаторът автоматично ще ги опрости и ще генерира код за следния оператор:

b [i] = (a[i] = (i+1)/2 + 1) + 1;
          

Пример 2.2. Еднократно смятане отместването в масив

Изразът

a [i] = a [i]/(a [i] + 1) * (a [i] - 1);
          

се свежда до

{int *x = a+i; int y = *x; *x = y/(y+1) * (y-1);}
          

Друг вид оптимизация, която правят компилаторите, е свеждането на аритметичните операции до по-прости. Няма смисъл сами да опростяваме аритметичните операции единствено с цел по-добра ефективност. Ефективността на преобразуванията, които ще направим, зависи до голяма степен от свойствата на използвания процесор. И може да се окаже, че когато компилираме програмата на процесор от друга фамилия (или от същата фамилия, но по-нов), тя ще работи по-бавно, отколкото ако не я бяхме „оптимизирали“ ръчно. По-добре да оставим тези оптимизации за компилатора, в повечето случаи той знае по-добре от нас свойствата на процесора.

Пример 2.3. Замяна на аритметични операции с по-ефективни

Изразът x*2 при процесори 80386 е най-добре да се опрости като x<<1. Но при всички по-нови процесори от тази фамилия по-бърз вариант се оказва x+x.

Можем да се доверим на уменията на съвременните компилатори — дори човек, програмиращ на Асемблер, с много труд би генерирал по-добър код. Някои компилатори се справят перфектно. ГНУ-компилаторът макар и не перфектен също се справя забележително добре, като се вземе предвид, че той не е оптимизирван за конкретен тип процесор, а може да генерира код за над 30 вида архитектури, повечето от които се разделят на няколко подтипа.