Стандарт 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 : valuecondition; value
Зіткнувшись з кодом у вашому запитанні, цей самий компілятор аналізує, що коли x = - xобчислюється, значення -xмає те, що є зручним. Тож призначення можна оптимізувати.
Я не шукав приклад компілятора, який поводиться так, як описано вище, але це такий тип оптимізації, який намагаються зробити хороші компілятори. Я б не здивувався, зустрівши таку. Ось менш правдоподібний приклад компілятора, з яким ваша програма аварійно завершує роботу. (Це може бути не так неправдоподібно, якщо ви компілюєте програму в якомусь розширеному режимі налагодження.)
Цей гіпотетичний компілятор відображає кожну змінну на іншій сторінці пам'яті та встановлює атрибути сторінки таким чином, що читання з неініціалізованої змінної спричиняє пастку процесора, яка викликає налагоджувач. Будь-яке призначення змінної спочатку гарантує, що її сторінка пам’яті нормально відображена. Цей компілятор не намагається виконати будь-яку розширену оптимізацію - він перебуває в режимі налагодження, призначеному для легкого пошуку таких помилок, як неініціалізовані змінні. При x = - xобчисленні права частина викликає пастку, і налагоджувач спрацьовує.