#gcc #compiler-optimization
#gcc #оптимизация компилятора
Вопрос:
В последние часы я отлаживал странную проблему, которая возникла только в сборке выпуска (-O3), но не в сборке отладки (-g и без оптимизации). Наконец, я мог бы связать это со встроенным «подсчетом конечных нулей», дающим мне неправильные результаты, и теперь я задаюсь вопросом, нашел ли я только что ошибку GCC или я что-то упустил.
Короткая история заключается в том, что, по-видимому, GCC __builtin_ctz
ошибочно вычисляет -O2 и -O3 в некоторых ситуациях, но он отлично справляется без оптимизации или -O1 . То же самое относится к длинным вариантам __builtin_ctzl
и __builtin_ctzll
.
Мое первоначальное предположение заключается в том, что __builtin_ctz(0)
оно должно быть равно 32, потому что это unsigned int
(32-разрядная) версия встроенного и, следовательно, имеет 32 завершающих нулевых бита. Я не нашел ничего, что указывало бы, что эти встроенные функции не определены для ввода, равного нулю, и практическая работа с ними убедила меня, что это не так.
Давайте посмотрим на код, о котором я хотел бы поговорить сейчас:
bool test_basic;
bool test_ctz;
bool test_resu<
int ctz(const unsigned int x) {
const int q = __builtin_clz(x);
test_ctz = (q == 32);
return q;
};
int main(int argc, char** argv) {
{
const int q = __builtin_clz(0U);
test_basic = (q == 32);
}
{
const int q = ctz(0U);
test_result = (q == 32);
}
std::cout << "test_basic=" << test_basic << std::endl;
std::cout << "test_ctz=" << test_ctz << std::endl;
std::cout << "test_result=" << test_result << std::endl;
}
Код в основном выполняет три теста, сохраняя результаты в этих логических значениях:
test_basic
имеет значение true, если__builtin_clz(0U)
принимает значение 32.test_ctz
имеет значение true, если__builtin_clz(x)
в функции равно 32ctz
.test_result
имеет значение true, если результатctz(0)
равен 32.
Поскольку я вызываю ctz
один раз в своей main
функции и передаю ей ноль, я ожидаю, что все три bools будут true к концу программы. На самом деле это так, если я скомпилирую его без каких-либо оптимизаций или -O1
. Однако, когда я компилирую его с -O2
помощью, test_ctz
становится false . Я обратился к обозревателю компилятора, чтобы выяснить, что, черт возьми, происходит. (Обратите внимание, что я сам использую g 7.5, но я мог бы воспроизвести это и в любой более поздней версии. В проводнике компилятора я выбрал последнюю версию, которую он может предложить, а именно 10.2.)
Давайте сначала посмотрим на код, скомпилированный с -O1 . Я вижу, что test_ctz
это просто значение равно 1. Я думаю, это потому, что эти встроенные компоненты обрабатываются как constexpr
, и вся довольно простая функция ctz
оценивается во время компиляции. Результат правильный (согласно моему первоначальному предположению), и поэтому меня это устраивает.
Итак, что может пойти не так отсюда? Что ж, давайте посмотрим на код, скомпилированный с -O2 . Ничего особенного не изменилось, просто test_ctz
теперь установлено значение 0! И это вне всякой логики: компилятор, по-видимому, оценивает q == 32
значение false , но затем q
возвращается из функции, и мы сравниваем это с 32, и вдруг это true ( test_result
) . У меня нет объяснения этому. Я что-то упускаю? Я нашел какую-то демоническую ошибку GCC?
Становится еще смешнее, если задано printf
значение q
just before test_ctz
: затем консоль выводит 32, поэтому вычисление фактически работает так, как ожидалось — во время выполнения. Тем не менее, во время компиляции компилятор считает q
, что это не 32, и test_ctz
вынужден принимать значение false . Действительно, если я изменю объявление q
from const int
на volatile int
и, таким образом, принудительно выполню вычисления во время выполнения, все будет работать так, как ожидалось, так что, к счастью, есть простой обходной путь.
В заключение я хотел бы отметить, что я также использую встроенные функции «подсчета начальных нулей» ( __builtin_clz
и длинные версии), и я не мог наблюдать там ту же проблему; они работают просто отлично.
Ответ №1:
Я не нашел ничего, что указывало бы, что эти встроенные функции не определены для ввода, равного нулю
Как вы могли это пропустить??? Из онлайн-документов gcc другие встроенные:
Встроенная функция: int __builtin_ctz (unsigned int x)
Возвращает количество конечных 0-бит в x, начиная с позиции младшего значащего бита. Если x равно 0, результат не определен.
Итак, что может пойти не так отсюда?
Поведение кода по-разному при разных уровнях оптимизации в 99% случаев является явным признаком неопределенного поведения в вашем коде. В этом случае оптимизация компилятора принимает разные решения, чем инструкция архитектуры BSR, и в случае, если компилятор генерирует архитектуру BSR
x86, результат по-прежнему не определен из ссылки If the content source operand is 0, the content of the destination operand is undefined
. Ох, в этом случае вы также получите LZCNT will produce the operand size when the input operand is zero
LZCNT , который, возможно, лучше объясняет поведение вашего кода.
Я что-то упускаю?
ДА. Вам не хватает того, что __builtin_ctz(0)
не определено.
Я нашел какую-то демоническую ошибку GCC?
Нет.
Я хотел бы отметить, что я также использую встроенные функции «подсчета начальных нулей» (__builtin_clz и длинные версии) Я не мог наблюдать там ту же проблему; они работают просто отлично.
Можно увидеть в документах gcc, что __builtin_clz(0)
также является неопределенным поведением.
Комментарии:
1. ХОРОШО, я честен здесь, я не знаю, как я мог это пропустить. Большое спасибо.
2. К настоящему времени я понял, как я это пропустил: (1) Я предположил, что это просто LZCNT , который, как вы сказали, выдает размер операнда, если входные данные равны нулю — именно то, что мне было нужно — и (2) Меня действительно интересовали только длинные версии, но обратите внимание, чтоbuiltin не определено для 0, задается только для версий unsigned int . Описание длинных версий относится только к ним, и, честно говоря, я этого не читал. Тем не менее, для моего минимального примера я решил использовать эту базовую версию. Так что да, это классический случай RTFM, но, по крайней мере, кажется, что я могу немного защитить себя. 🙂