Стандарт C дає компіляторам багато свободи для виконання оптимізацій. Наслідки цих оптимізацій можуть здивувати, якщо ви припустите наївну модель програм, де для неініціалізованої пам'яті встановлено якийсь випадковий бітовий шаблон і всі операції виконуються в тому порядку, в якому вони написані.
Примітка: наведені нижче приклади є дійсними лише тому, що x
ніколи не брали їх адреси, тому вони є "подібними до реєстру". Вони також були б дійсними, якби тип x
мав уявлення про пастки; це рідко буває для непідписаних типів (для цього потрібно «витратити» принаймні один біт сховища, і це має бути задокументовано), і це неможливо unsigned char
. Якби x
мав тип зі знаком, тоді реалізація могла б визначити бітовий шаблон, який не є числом від - (2 n-1 -1) до 2 n-1 -1, як представлення пастки. Дивіться відповідь Єнса Густедта .
Компілятори намагаються присвоїти регістри змінним, оскільки регістри швидші за пам'ять. Оскільки програма може використовувати більше змінних, ніж у процесора є регістри, компілятори виконують розподіл регістрів, що призводить до різних змінних, використовуючи один і той же регістр у різний час. Розглянемо фрагмент програми
unsigned x, y, z;
y = 0;
z = 4;
x = - x;
y = y + z;
x = y + 1;
Коли рядок 3 обчислюється, він x
ще не ініціалізований, тому (з причини компілятора) рядок 3 повинен бути якоюсь випадковістю, яка не може відбутися через інші умови, які компілятор не був достатньо розумним, щоб зрозуміти. Оскільки z
не використовується після рядка 4 і x
не використовується перед рядком 5, один і той же регістр може використовуватися для обох змінних. Отже, ця невеличка програма компілюється до наступних операцій над регістрами:
r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
Кінцеве значення x
- це кінцеве значення r0
, а кінцеве значення y
- це кінцеве значення r1
. Ці значення складають x = -3 та y = -4, а не 5 та 4, як це сталося б, x
якби було належним чином ініціалізовано.
Для більш детального прикладу розглянемо такий фрагмент коду:
unsigned i, x;
for (i = 0; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Припустимо, що компілятор виявляє, що condition
не має побічних ефектів. Оскільки condition
не змінюється x
, компілятор знає, що перший цикл циклу не може бути доступним, x
оскільки він ще не ініціалізований. Тому перше виконання тіла циклу еквівалентно x = some_value()
, немає необхідності перевіряти стан. Компілятор може скомпілювати цей код так, ніби ви написали
unsigned i, x;
i = 0;
x = some_value();
for (i = 1; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Це може бути змодельовано всередині компілятора, щоб врахувати, що будь-яке значення, яке залежить від, x
має будь-яке значення, яке є зручним , якщо x
воно не ініціалізоване. Оскільки поведінка, коли неініціалізована змінна є невизначеною, а не змінна, яка має просто невстановлене значення, компілятору не потрібно відстежувати будь-які спеціальні математичні взаємозв'язки між будь-якими зручними значеннями. Таким чином, компілятор може проаналізувати наведений вище код таким чином:
- під час першої ітерації циклу
x
неініціалізована до часу -x
обчислення.
-x
має невизначену поведінку, тому його значення є чим завгодно-зручним.
- Застосовується правило оптимізації , тому цей код можна спростити .
condition ? value : value
condition; value
Зіткнувшись з кодом у вашому запитанні, цей самий компілятор аналізує, що коли x = - x
обчислюється, значення -x
має те, що є зручним. Тож призначення можна оптимізувати.
Я не шукав приклад компілятора, який поводиться так, як описано вище, але це такий тип оптимізації, який намагаються зробити хороші компілятори. Я б не здивувався, зустрівши таку. Ось менш правдоподібний приклад компілятора, з яким ваша програма аварійно завершує роботу. (Це може бути не так неправдоподібно, якщо ви компілюєте програму в якомусь розширеному режимі налагодження.)
Цей гіпотетичний компілятор відображає кожну змінну на іншій сторінці пам'яті та встановлює атрибути сторінки таким чином, що читання з неініціалізованої змінної спричиняє пастку процесора, яка викликає налагоджувач. Будь-яке призначення змінної спочатку гарантує, що її сторінка пам’яті нормально відображена. Цей компілятор не намагається виконати будь-яку розширену оптимізацію - він перебуває в режимі налагодження, призначеному для легкого пошуку таких помилок, як неініціалізовані змінні. При x = - x
обчисленні права частина викликає пастку, і налагоджувач спрацьовує.