Почему эта оптимизация компилятора происходит только вне основного?

У меня есть следующая функция 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?

@DavisHerring: Re «Зачем пытаться это понять?»: Потому что поведение, определенное стандартом C, — не единственный аспект поведения компьютера, который нас интересует. Понимание поведения, выходящего за рамки того, что определяет стандарт C, полезно для отладки, оптимизации и более.

Eric Postpischil 01.06.2024 08:31

Попробуйте unsigned int x = (unsigned int)a * b; Shorts повышаются до int только после умножения во время присваивания.

fukanchik 01.06.2024 09:03

Это не «только внутри функции». main() — это тоже функция. Разница здесь заключается в разнице между аргументами и локальными переменными, инициализированными константами.

user207421 01.06.2024 09:10

«Понимание поведения, выходящего за рамки того, что определено стандартом C, полезно для отладки, оптимизации и многого другого». - Действительно? Если вы пишете код, который зависит от неопределенного поведения, он может сломаться при следующем изменении версии компилятора, когда вы запускаете код на другом оборудовании, когда компьютер находится под (другой) нагрузкой, когда ветер меняет направление. Если ваша потребность в производительности настолько критична, что вам приходится писать код с неопределенным поведением, вам, вероятно, следует кодировать критические для производительности части на языке ассемблера!

Stephen C 01.06.2024 09:21

@StephenC: Re «Правда?»: Да, правда. Различные ошибки в коде проявляют различные симптомы, а знание и опыт поведения компилятора (а также операционной системы, процессора и т. д.) превращают эти симптомы в подсказки об ошибках. Ошибочные данные в памяти, нарушения сегментов и ошибочный вывод — это симптомы разных вещей. Отладка с такими знаниями выполняется быстрее и, следовательно, дешевле, чем случайное угадывание того, в чем может заключаться ошибка. Учтите, что практически все, что отладчик показывает пользователю, не определено стандартом C, но очень полезно для отладки.

Eric Postpischil 01.06.2024 10:08

@StephenC: Re: «Если вы пишете код, который зависит от неопределенного поведения, он может сломаться при следующем изменении версии компилятора»: это неправильная характеристика того, что такое «неопределенное поведение», определенное стандартом C, и этого не должно быть. учил. Стандарт C оставляет некоторые вещи неопределенными, потому что у авторов не было разумного определения, другие - потому что были разумные определения, но они различались в зависимости от реализации, третьи - потому что они были предназначены для того, чтобы реализации могли определить их, если захотят, а третьи - потому что они область других спецификаций…

Eric Postpischil 01.06.2024 10:11

… Поэтому неверно продвигать идею о том, что «неопределённое поведение» — это поведение, которого следует избегать или которое может меняться случайным образом. «Неопределенное поведение» означает только «не охваченное стандартом C». Некоторые из них определены другими документами и не будут меняться случайным образом. И даже многое из того, что не указано, тем не менее является следствием конструкции компилятора и во многом следует предсказуемым шаблонам, которые полезны для диагностики ошибок.

Eric Postpischil 01.06.2024 10:13

Обычно оптимизатор сначала исключает вызов функции, встраивая ее, а затем исключает умножение, заранее вычисляя результат. Это вычисление не подвержено такому же поведению при переполнении. Но с -O1 этого не может произойти, встраивание создает больше кода, поэтому удаление умножения также невозможно.

Hans Passant 01.06.2024 14:03

Вероятно, проблема здесь не в этом, а в IIRC, одна из причин, по которой main может быть оптимизирована иначе, чем другие функции, заключается в том, что некоторые реализации будут рассматривать ее как «холодную» функцию, поскольку ее можно вызвать только один раз. (Да, технически это можно вызвать рекурсивно, но это почти никогда не делается). Таким образом, они могут оптимизировать его по размеру, а не по скорости.

Nate Eldredge 01.06.2024 17:10
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
128
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Внутри функции 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. Для этого значения установлен старший бит, поэтому компилятор генерирует код на этой основе.

Такое поведение зависит от функций компилятора и параметров оптимизации. Вышеизложенное является объяснением того, что произошло в конкретных обстоятельствах, которые вы изучили, но поведение может отличаться в других обстоятельствах.

Другие вопросы по теме