GCC: неправильная оценка __builtin_ctz во время компиляции в некоторых ситуациях с -O2 и -O3

#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;
}
  

Код в основном выполняет три теста, сохраняя результаты в этих логических значениях:

  1. test_basic имеет значение true, если __builtin_clz(0U) принимает значение 32.
  2. test_ctz имеет значение true, если __builtin_clz(x) в функции равно 32 ctz .
  3. 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, но, по крайней мере, кажется, что я могу немного защитить себя. 🙂