Я столкнулся с проблемой, когда 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
Мои вопросы:
Обязательно к прочтению: Неопределённое поведение может привести к путешествию во времени (кроме всего прочего, но путешествие во времени — самое интересное)
Пробовал на Godbolt. Не могу воспроизвести проблему. Ваши данные компиляторы возвращают ожидаемые результаты...
@MarkRansom, можете ли вы указать на неопределенное поведение в опубликованном коде?
@ÖöTiib, если бы я мог, я бы оставил ответ вместо комментария. Но статья по-прежнему полна примеров того, как компиляторы могут с помощью оптимизации переделать разумный код в бессмыслицу.
@MarkRansom, это действительно отличная статья, но все случаи в ней являются результатом недоопределенного поведения, которое мы можем определить в коде. Как вы думаете, здесь дело в этом? Признаюсь, мне не удалось обнаружить ни одного.
@wohlstad Я тоже не заметил ни одного, но неопределенное поведение может быть чрезвычайно тонким! Это действительно кажется возможным, хотя судя по симптомам.
@MarkRansom Другая возможность - это ошибка компилятора, особенно с учетом того, что gcc-9 верен, gcc-10 неверен, gcc-11 неверен и gcc-12 и выше снова верны.
Похоже, что ответы на ваши вопросы: да, это известная проблема, обновите свой компилятор.
Спасибо всем за помощь. Однако несовместимость с g++ 10 и 11 не является небольшим ограничением. На самом деле, несовместимость с g++ по умолчанию во многих используемых в настоящее время дистрибутивах Linux — это не то, что я бы рассматривал как решение.
Я имею в виду, что godbolt.org/z/az13dWWYx не содержит ошибки и логически идентичен. Как можно предположить, компилятор замечает, что ваш максимальный тест не меняет значение cp_new
— ветви идентичны. Так что это оптимизирует его. Затем он делает cp_new < *x
где cp_new = *x-1
и говорит: «Недополнение — это UB, поэтому мы можем его игнорировать», таким образом, ответ всегда верен: он использует UB с известным результатом для оптимизации, затем использует тот факт, что одна ветвь является UB для повторной оптимизации (в ошибка). Вместо этого моя версия явно проверяет наличие сбоя.
@MarkRansom: Использование constexpr
запрещает большинство UB, и все они принимают код Демо. Таким образом, у ошибочных значений значения времени выполнения отличаются от значений времени компиляции.
Они могут быть дорогими, но -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 Да, это ошибка компилятора. Это исправлено в gcc 12. ОП тоже знает, что это ошибка, речь идет об устранении проблемы без обновления компилятора. Я просто даю сказочное объяснение того, в чем может быть ошибка и почему изменение флагов переполнения, похоже, ее исправляет.
Похоже на ошибку, появившуюся в gcc-10 и исправленную в gcc-12, но я не могу найти нужный билет. Воспроизведение на godbolt: godbolt.org/z/vhTPEPz8c