G++ оптимизирует проверку INT_MIN в сборке выпуска

Я столкнулся с проблемой, когда g++ оптимизировал то, чего не должно было быть. Я свел проблему к следующему примеру: У меня есть статическая библиотека с функцией bool my_magic_function(int* x), которая уменьшает x на 1, если может, в противном случае (x == INT_MIN) она возвращает false и не затрагивает исходное значение. Если я использую эту функцию в отладочной сборке, она работает как положено. Но в релизной сборке проверка оптимизирована. Платформа:

В RHEL 9.3 с g++ (GCC) 11.4.1 20230605 -> Проблема присутствует.

Ubuntu 22.04 g++ 11.4.0 g++ или 10.5.0 g++ -> Имеется проблема

Ubuntu 22.04 g++ 9.5.0 -> Код работает так, как и ожидалось, в выпуске.

Вот минимальный пример со статической библиотекой и простым main.cpp с использованием функции:

almalib.h:

bool my_magic_function(int* x); 

almalib.cc:

#include "almalib.h"
#include <cstring>
#include <limits>

bool my_magic_function(int* x) {
    int cp_new;
    // integer overflow is undefined, so lets make sure it becomes int max
    if (*x == std::numeric_limits<int>::lowest()) {
        cp_new = std::numeric_limits<int>::max(); 
    } else {
        cp_new = *x - 1;
    }  
    if (cp_new < *x) {
        *x = cp_new;
        return true;    
    }
    return false;
} 

main.cpp

#include "almalib.h"
#include <iostream>
#include <limits>

int main()
{
    for (int x : {0, std::numeric_limits<int>::lowest()})
    {
        int x2 = x;
        std::cerr << "Res for " << x << " " << (my_magic_function(&x2) ? "OK" : "NOT_OK") << " val: " << x2 << std::endl;
    }
}

Скомпилировать:

g++ -c almalib.cc -o almalib.o
ar crf libalma.a almalib.o
g++ main.cpp -o run -L. -lalma

g++ -c almalib.cc -O3 -o almalibR.o
ar crf libalmaR.a almalibR.o
g++ main.cpp -O3 -o runR -L. -lalmaR

вывод для отладки (./run):

Res for 0 OK val: -1
Res for -2147483648 NOT_OK val: -2147483648

вывод для Release (./runR):

Res for 0 OK val: -1
Res for -2147483648 OK val: 2147483647

просматривая сгенерированную сборку с помощью gdb, my_magic_function сокращается до 3 строк:

0x401320 <_Z17my_magic_functionPi>      subl   $0x1,(%rdi)                                                                                                                                                                                          
0x401323 <_Z17my_magic_functionPi+3>    mov    $0x1,%eax                                                                                                                                                                                                     
0x401328 <_Z17my_magic_functionPi+8>    ret               

Мои вопросы:

  • Это известная проблема?
  • Какие у меня есть варианты, чтобы этого не произошло? (Я могу тривиально переписать примерную функцию, но не исходную задачу). Есть ли какие-либо подсказки компилятора или мне следует отключить определенный тип оптимизации?

Похоже на ошибку, появившуюся в gcc-10 и исправленную в gcc-12, но я не могу найти нужный билет. Воспроизведение на godbolt: godbolt.org/z/vhTPEPz8c

Yksisarvinen 03.05.2024 13:19

Пробовал на Godbolt. Не могу воспроизвести проблему. Ваши данные компиляторы возвращают ожидаемые результаты...

Klaus 03.05.2024 13:21

@MarkRansom, можете ли вы указать на неопределенное поведение в опубликованном коде?

Öö Tiib 03.05.2024 13:22

@ÖöTiib, если бы я мог, я бы оставил ответ вместо комментария. Но статья по-прежнему полна примеров того, как компиляторы могут с помощью оптимизации переделать разумный код в бессмыслицу.

Mark Ransom 03.05.2024 13:24

@MarkRansom, это действительно отличная статья, но все случаи в ней являются результатом недоопределенного поведения, которое мы можем определить в коде. Как вы думаете, здесь дело в этом? Признаюсь, мне не удалось обнаружить ни одного.

wohlstad 03.05.2024 13:48

@wohlstad Я тоже не заметил ни одного, но неопределенное поведение может быть чрезвычайно тонким! Это действительно кажется возможным, хотя судя по симптомам.

Mark Ransom 03.05.2024 13:59

@MarkRansom Другая возможность - это ошибка компилятора, особенно с учетом того, что gcc-9 верен, gcc-10 неверен, gcc-11 неверен и gcc-12 и выше снова верны.

Yksisarvinen 03.05.2024 14:11

Похоже, что ответы на ваши вопросы: да, это известная проблема, обновите свой компилятор.

Eljay 03.05.2024 14:44

Спасибо всем за помощь. Однако несовместимость с g++ 10 и 11 не является небольшим ограничением. На самом деле, несовместимость с g++ по умолчанию во многих используемых в настоящее время дистрибутивах Linux — это не то, что я бы рассматривал как решение.

Zoltán Mártonka 03.05.2024 15:41

Я имею в виду, что godbolt.org/z/az13dWWYx не содержит ошибки и логически идентичен. Как можно предположить, компилятор замечает, что ваш максимальный тест не меняет значение cp_new — ветви идентичны. Так что это оптимизирует его. Затем он делает cp_new < *x где cp_new = *x-1 и говорит: «Недополнение — это UB, поэтому мы можем его игнорировать», таким образом, ответ всегда верен: он использует UB с известным результатом для оптимизации, затем использует тот факт, что одна ветвь является UB для повторной оптимизации (в ошибка). Вместо этого моя версия явно проверяет наличие сбоя.

Yakk - Adam Nevraumont 03.05.2024 15:48

@MarkRansom: Использование constexpr запрещает большинство UB, и все они принимают код Демо. Таким образом, у ошибочных значений значения времени выполнения отличаются от значений времени компиляции.

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

Ответы 1

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

Они могут быть дорогими, но -fwrapv и -ftrapv заставят вашу проблему исчезнуть.

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

С любым флагом ваш код работает правильно.

Хотя -fwrapv кажется безобидным, это означает, что множество оптимизаций в циклах и сравнениях выполнить невозможно.

Без -fwrapv компилятор может предположить, что a+b, где значение больше 0, больше a и больше b. С ним не может.

Как можно предположить, ваш компилятор сначала берет код раннего ветвления.

if (*x == std::numeric_limits<int>::lowest()) {
    cp_new = std::numeric_limits<int>::max(); 
} else {
    cp_new = *x - 1;
}

и говоря: «На аппаратном уровне это эквивалентно»

cp_new = *x - 1;

потому что он знает, что аппаратная цель имеет подписанное нижнее переполнение, которое зацикливается. Значительная оптимизация, устраняет ненужные ветки!

Затем он смотрит на

if (cp_new < *x) {
    *x = cp_new;
    return true;    
}

затем заменяет cp_new:

if ((*x - 1)< *x) {
    *x = (*x - 1);
    return true;    
}

и причины: «ну, знаковое неполное значение - это неопределенное поведение, поэтому что-то минус 1 всегда меньше чего-то». Таким образом, оптимизируя его в:

*x = *x-1;
return true;    

ошибка заключалась в том, что он использовал cp_new = *x - 1 в контексте, где определено нижнее переполнение и сначала переносится, а затем повторно использовалось, не допуская переноса.

Делая ошибку, вызывающую ловушку, или предполагая, что это правда, мы блокируем предположения, которые позволяют выполнить вторую ложную оптимизацию.

Но эта история - почему fwrapv/ftrapv работает - это «просто история», она не основана на фактическом чтении кода gcc или отчетов об ошибках; Я сделал предположение о причине ошибки, что привело к идее поиграться с настройками переполнения, что действительно устранило ваши симптомы. Считайте это сказкой, объясняющей, почему -fwrapv исправляет вашу ошибку.

Это не может быть правильно; компилятор не может оптимизировать ветвление, утверждая, что оно такое же и в целевой архитектуре, тем самым вводя неопределенное поведение, а затем используя это обоснование для оптимизации большего количества кода. Я ожидаю, что это просто ошибка компилятора.

ShdNx 07.05.2024 23:55

@ShdNx Да, это ошибка компилятора. Это исправлено в gcc 12. ОП тоже знает, что это ошибка, речь идет об устранении проблемы без обновления компилятора. Я просто даю сказочное объяснение того, в чем может быть ошибка и почему изменение флагов переполнения, похоже, ее исправляет.

Yakk - Adam Nevraumont 08.05.2024 15:43

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