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