Проблеми

За съжаление нещата не винаги са толкова гладки, колкото се опитахме да покажем в предходния раздел. Да си припомним пример 4 от предния раздел. В него

for (i = 0; i < 10000; i++) {
    a [i] = i;
    if (u != 0) a [i] = 0;
        b [i] = 2 + a [i];
}
        

се опростява на

if (u != 0)
    for (i = 0; i < 10000; i++) {
        a [i] = 0;
        b [i] = 2;
    }
} else {
    for (i = 0; i < 10000; i++) {
        a [i] = i;
        b [i] = 2 + i;
    }
}
        

Но какво ще стане, ако &u е равно например на a + 2976? В този случай операторът a[2976] = 0 ще присвои стойност на променливата u, което ще направи оптимизационното преобразувание некоректно.

За съжаление компилаторът няма от къде да разбере, че програмистът не е използвал в програмата си точната връзка между a и &u. Разбира се, да се използва в програмата по такъв неясен начин тази връзка е „трик“, който един опитен програмист не би си позволил. Компилаторът обаче откъде може да знае това? И затова не оптимизира.

А действителното положение на нещата дори още по-лошо! Компилаторът не само, че няма да оптимизира програмата, но напротив — ще генерира крайно неефективен код. При всяко използване на указател променливите, които се намират в регистрите на процесора, ще трябва да се прехвърлят в оперативната памет и после пак да се прочитат от там. Излиза, че компилираме програмата така, сякаш процесорът няма регистри.

Ще поясним това по-подробно. Нека в даден момент в програмата се използва обръщане към указател *p или масив a[i], а v е променлива, която в този момент се намира в регистър на процесора. В този момент компилаторът „не знае“ дали p или съответно a+i не е равно на &v. Ако това е така, то *p или съответно a[i] ще бъде равно на v. И за да не възникне проблем във връзка с това, че в този момент променливата v се намира не на определеното й място в оперативната памет, а в регистър, компилаторът е принуден да запише този регистър в оперативната памет. При това ако след това в програмата потрябва стойността на тази променлива, може да се наложи тя да бъде прочетена от оперативната памет, защото няма да е ясно, че регистъра съдържа актуалната й стойност.

За щастие има начини за справяне с този проблем. Най-добрият начин за справяне с този проблем според мене е да се използва език от по-високо ниво без аритметика с указатели[7]. Сега обаче ще разгледаме различни начини, които може да използваме при използване на най-разпространените в момента езици — Си и неговото разширение Си++.

Един от начините компилаторът да разбера, че програмистът не използва трикове, свързани с връзката между указател p и адресът на променлива v, е да се направи ясно, че никога не е получаван адресът на променливата v. Това условие е най-лесно изпълнимо, ако променливата е локална — тогава компилаторът просто трябва да прегледа текста на функцията, в която е дефинирана v, за да се увери, че не е пресмятан адресът v или както се казва още — че v е без псевдоними[8].

Нека отново разгледаме пример 4. При него компилаторът не знае дали програмистът не използва това, че за някое i a+i е равно на &u. Нека обаче променливата u е локална и програмистът никога не е пресмятал стойността на &u в програмата. В такъв случай няма начин програмистът да използва връзката между a и &u. Затова в този случай компилаторът може да оптимизира.

А какво да правим в случай, че се налага да използваме глобална променлива? Твърде често при използване на глобални променливи кодът не може да се оптимизира добре, а самите променливи, както показахме, трудно могат за дълго да се задържат в регистър на процесора. Начинът за справяне с този проблем е временно да използваме локална променлива. Например, ако в пример 4 променливата u е глобална, вместо нея по следния начин можем да използваме локална променлива:

{
    int v = u;
    for (i = 0; i < 10000; i++) {
        a [i] = i;
        if (v != 0) a [i] = 0;
        b [i] = 2 + a [i];
    }
}
        

За съжаление не всичко може да бъде локална променлива, така че на компилаторът да бъде ясно, че не използваме по някакъв начин адреса му. Например масивите рядко се намират в стека, така че използването им често пречи на компилаторът да оптимизира. В следващия раздел ще разгледаме по-подробно проблемите, които могат да възникнат в тази връзка, както и начините за справяне с тях.



[7] Не бива да оценяваме езиците за програмиране по техния синтаксис. Така например няма голяма разлика между Си и съвременните варианти на Паскал — и при двата езика може да се използва аритметика с указатели, в който случай и при двата езика добрият компилатор ще генерира еднакво ефективен или неефективен код. От друга страна на пръв поглед Джава прилича много на Си. Липсата на указатели обаче съществено улеснява извършването на локални и глобални оптимизационни преобразувания. И ако този език е по-бавен от Си, това не е поради това, че той се интерпретира. ГНУ-компилаторът например може да компилира програмите на Джава до машинен код. Джава е бавна поради динамичния характер на обектите, при който имаме непредсказуемо отложено свързване, а в резултат не може да се извърши и никаква междупроцедурна оптимизация. Айфел е език, който поне на теория позволява извършването на разнообразни междупроцедурни оптимизационни преобразувания. Теорията за тях обаче все още не е напълно разработена, така че не можем в близките няколко години да очакваме компилатор на Айфел, който да генерира по-добър код от компилаторите на Си.

[8] По-нататък ще видим, че глобални променливи трябва да се използват само при крайна необходимост. Още сега обаче имаме един малък довод срещу използването на глобални променливи.