Standard C daje kompilatorom dużą swobodę w przeprowadzaniu optymalizacji. Konsekwencje tych optymalizacji mogą być zaskakujące, jeśli przyjmie się naiwny model programów, w których niezainicjowana pamięć jest ustawiona na jakiś losowy wzorzec bitowy, a wszystkie operacje są wykonywane w kolejności, w jakiej zostały zapisane.
Uwaga: poniższe przykłady są poprawne tylko dlatego, x
że jego adres nigdy nie został zajęty, więc jest „podobny do rejestru”. Byłyby również ważne, gdyby typ x
miał reprezentacje pułapki; rzadko ma to miejsce w przypadku typów bez znaku (wymaga to „marnowania” co najmniej jednego bitu pamięci i musi być udokumentowane) i niemożliwe w przypadku unsigned char
. Gdyby x
miał typ ze znakiem, to implementacja mogłaby zdefiniować wzór bitowy, który nie jest liczbą między - (2 n-1 -1) a 2 n-1 -1 jako reprezentację pułapki. Zobacz odpowiedź Jensa Gustedta .
Kompilatory próbują przypisać rejestry do zmiennych, ponieważ rejestry są szybsze niż pamięć. Ponieważ program może wykorzystywać więcej zmiennych niż procesor posiada rejestry, kompilatory dokonują alokacji rejestrów, co prowadzi do różnych zmiennych wykorzystujących ten sam rejestr w różnym czasie. Rozważ fragment programu
unsigned x, y, z;
y = 0;
z = 4;
x = - x;
y = y + z;
x = y + 1;
Kiedy wiersz 3 jest oceniany, x
nie jest jeszcze zainicjowany, dlatego (uzasadnia kompilator) wiersz 3 musi być jakimś przypadkiem, który nie może się zdarzyć z powodu innych warunków, których kompilator nie był wystarczająco inteligentny, aby dowiedzieć się. Ponieważ z
nie jest używany po linii 4 i x
nie jest używany przed linią 5, ten sam rejestr może być używany dla obu zmiennych. Tak więc ten mały program jest skompilowany do następujących operacji na rejestrach:
r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
Końcowa wartość x
to końcowa wartość r0
, a końcowa wartość y
to końcowa wartość r1
. Te wartości to x = -3 i y = -4, a nie 5 i 4, jak by się stało, gdyby x
został poprawnie zainicjowany.
Aby uzyskać bardziej rozbudowany przykład, rozważ następujący fragment kodu:
unsigned i, x;
for (i = 0; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Załóżmy, że kompilator wykryje, że condition
nie ma to żadnego efektu ubocznego. Ponieważ condition
nie modyfikuje x
, kompilator wie, że pierwszy przebieg pętli nie może uzyskać dostępu, x
ponieważ nie został jeszcze zainicjowany. Dlatego pierwsze wykonanie treści pętli jest równoważne x = some_value()
, nie ma potrzeby testowania warunku. Kompilator może skompilować ten kod tak, jakbyś to napisał
unsigned i, x;
i = 0;
x = some_value();
for (i = 1; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Sposób, w jaki można to modelować w kompilatorze, polega na rozważeniu, że każda wartość zależna od x
ma jakąkolwiek wartość jest wygodna, o ile nie x
jest zainicjowana. Ponieważ zachowanie, gdy niezainicjowana zmienna jest niezdefiniowana, a nie zmienna ma jedynie nieokreśloną wartość, kompilator nie musi śledzić żadnych specjalnych matematycznych relacji między wartościami, które są wygodne. Dlatego kompilator może przeanalizować powyższy kod w następujący sposób:
- podczas pierwszej iteracji pętli nie
x
jest inicjowany do czasu -x
oceny.
-x
ma niezdefiniowane zachowanie, więc jego wartość jest taka, jaka jest-wygodna.
- Obowiązuje reguła optymalizacji , więc ten kod można uprościć do .
condition ? value : value
condition; value
W konfrontacji z kodem w twoim pytaniu, ten sam kompilator analizuje, że kiedy x = - x
jest oceniany, wartość -x
jest cokolwiek-jest-wygodne. Dzięki temu można zoptymalizować przypisanie.
Nie szukałem przykładu kompilatora, który zachowuje się tak, jak opisano powyżej, ale jest to rodzaj optymalizacji, który dobre kompilatory próbują wykonać. Nie zdziwiłbym się, gdyby takiego spotkałem. Oto mniej prawdopodobny przykład kompilatora, z którym program ulega awarii. (Może to nie być takie nieprawdopodobne, jeśli kompilujesz swój program w jakimś zaawansowanym trybie debugowania).
Ten hipotetyczny kompilator mapuje każdą zmienną na innej stronie pamięci i ustawia atrybuty strony w taki sposób, że odczyt z niezainicjowanej zmiennej powoduje pułapkę procesora, która wywołuje debugger. Każde przypisanie do zmiennej najpierw upewnia się, że jej strona pamięci jest odwzorowana normalnie. Ten kompilator nie próbuje wykonywać żadnej zaawansowanej optymalizacji - działa w trybie debugowania, mającym na celu łatwe lokalizowanie błędów, takich jak niezainicjowane zmienne. Gdy x = - x
jest oceniany, prawa strona powoduje pułapkę i uruchamia debuger.