Стандарт 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выполняется оценка, правая сторона вызывает прерывание и запускается отладчик.