У меня есть следующая функция C, которая умножает два коротких слова без знака и сохраняет результат в целое число без знака. Затем эта функция выводит, имеет ли результирующее значение установленный старший бит или нет.
void unsigned_short_mult_test(unsigned short a, unsigned short b) {
unsigned int x = a * b;
if (x >= 0x80000000)
printf("%u >= %u", x, 0x80000000);
else
printf("%u < %u", x, 0x80000000);
}
Для теста я передаю значение 65535 для a и b в main():
int main() {
unsigned short a = 65535;
unsigned short b = 65535;
unsigned_short_mult_test(a, b);
return 0;
}
При включенной оптимизации компилятора (-O1 или выше) это всегда печатается «неправильно»:
4294836225 < 2147483648
Однако для меня это имеет смысл, потому что кажется, что компилятор преобразует a и b в целые числа во время умножения, а затем преобразует результат обратно в беззнаковое целое число для сохранения в x. Оптимизация предполагает, что произведение двух целых чисел не может быть больше максимального значения целого числа, и просто удаляет первую часть оператора if из полученного машинного кода. Однако когда я помещаю тот же код в main, такой оптимизации не происходит:
int main() {
unsigned short a = 65535;
unsigned short b = 65535;
// The same code as unsigned_short_mult_test
unsigned int x = a * b;
if (x >= 0x80000000)
printf("%u >= %u", x, 0x80000000);
else
printf("%u < %u", x, 0x80000000);
return 0;
}
Это печатает правильный результат: 4294836225 >= 2147483648
Почему эта оптимизация, кажется, происходит только в функции, а не когда она выполняется непосредственно в main?
Попробуйте unsigned int x = (unsigned int)a * b; Shorts повышаются до int только после умножения во время присваивания.
Это не «только внутри функции». main() — это тоже функция. Разница здесь заключается в разнице между аргументами и локальными переменными, инициализированными константами.
«Понимание поведения, выходящего за рамки того, что определено стандартом C, полезно для отладки, оптимизации и многого другого». - Действительно? Если вы пишете код, который зависит от неопределенного поведения, он может сломаться при следующем изменении версии компилятора, когда вы запускаете код на другом оборудовании, когда компьютер находится под (другой) нагрузкой, когда ветер меняет направление. Если ваша потребность в производительности настолько критична, что вам приходится писать код с неопределенным поведением, вам, вероятно, следует кодировать критические для производительности части на языке ассемблера!
@StephenC: Re «Правда?»: Да, правда. Различные ошибки в коде проявляют различные симптомы, а знание и опыт поведения компилятора (а также операционной системы, процессора и т. д.) превращают эти симптомы в подсказки об ошибках. Ошибочные данные в памяти, нарушения сегментов и ошибочный вывод — это симптомы разных вещей. Отладка с такими знаниями выполняется быстрее и, следовательно, дешевле, чем случайное угадывание того, в чем может заключаться ошибка. Учтите, что практически все, что отладчик показывает пользователю, не определено стандартом C, но очень полезно для отладки.
@StephenC: Re: «Если вы пишете код, который зависит от неопределенного поведения, он может сломаться при следующем изменении версии компилятора»: это неправильная характеристика того, что такое «неопределенное поведение», определенное стандартом C, и этого не должно быть. учил. Стандарт C оставляет некоторые вещи неопределенными, потому что у авторов не было разумного определения, другие - потому что были разумные определения, но они различались в зависимости от реализации, третьи - потому что они были предназначены для того, чтобы реализации могли определить их, если захотят, а третьи - потому что они область других спецификаций…
… Поэтому неверно продвигать идею о том, что «неопределённое поведение» — это поведение, которого следует избегать или которое может меняться случайным образом. «Неопределенное поведение» означает только «не охваченное стандартом C». Некоторые из них определены другими документами и не будут меняться случайным образом. И даже многое из того, что не указано, тем не менее является следствием конструкции компилятора и во многом следует предсказуемым шаблонам, которые полезны для диагностики ошибок.
Обычно оптимизатор сначала исключает вызов функции, встраивая ее, а затем исключает умножение, заранее вычисляя результат. Это вычисление не подвержено такому же поведению при переполнении. Но с -O1 этого не может произойти, встраивание создает больше кода, поэтому удаление умножения также невозможно.
Вероятно, проблема здесь не в этом, а в IIRC, одна из причин, по которой main может быть оптимизирована иначе, чем другие функции, заключается в том, что некоторые реализации будут рассматривать ее как «холодную» функцию, поскольку ее можно вызвать только один раз. (Да, технически это можно вызвать рекурсивно, но это почти никогда не делается). Таким образом, они могут оптимизировать его по размеру, а не по скорости.





Внутри функции unsigned_short_mult_test компилятор не знает, какие значения аргументов будут переданы, и выполняет абстрактный анализ. Вероятно, анализ определяет, что единственными случаями с определенным поведением являются те, для которых x имеет неотрицательное значение int, и поэтому компилятор может генерировать код, как если бы старший бит x никогда не был установлен.
Внутри main компилятор знает, какие значения используются, и вычисляет арифметику, используя эти конкретные значения. Вероятно, он умножает 65 535 на 65 535, позволяя результату переноситься (4 294 836 225 как unsigned, −131 071 как int) без учета того факта, что поведение не определено стандартом C), и определение x будет иметь значение 4 294 836 225. Для этого значения установлен старший бит, поэтому компилятор генерирует код на этой основе.
Такое поведение зависит от функций компилятора и параметров оптимизации. Вышеизложенное является объяснением того, что произошло в конкретных обстоятельствах, которые вы изучили, но поведение может отличаться в других обстоятельствах.
@DavisHerring: Re «Зачем пытаться это понять?»: Потому что поведение, определенное стандартом C, — не единственный аспект поведения компьютера, который нас интересует. Понимание поведения, выходящего за рамки того, что определяет стандарт C, полезно для отладки, оптимизации и более.