Неправильная оптимизация GCC 9 (и выше) для memcmp с -fno-inline

#c #gcc #compiler-optimization

#c #gcc #оптимизация компилятора

Вопрос:

Существует небольшая func функция, которая сравнивает блок памяти со статическим массивом с постоянным нулевым значением. Вот примитивный пример, иллюстрирующий проблему:

 #include <cstring>
#include <memory>

#define MAX_BYTES (256)

inline int my_memcmp(const void * mem1, const void * mem2, const size_t size)
{
    const auto *first  = reinterpret_cast<const uint8_t *>(mem1);
    const auto *second = reinterpret_cast<const uint8_t *>(mem2);
    if (size < 8)
    {
        for (int i = 0; i < size;   i) {
            if (*first != *second) return (*first > *second) ? 1 : -1;
              first;   second;
        }
        return 0;
    }

    return std::memcmp(mem1, mem2, size);
}

bool func(const uint8_t* in, size_t size)
{
  size_t remain = size;
  static const uint8_t zero_arr[MAX_BYTES] = { 0 };

  while (remain >= MAX_BYTES)
  {
    if (my_memcmp(in, zero_arr, MAX_BYTES) != 0)
    {
      return false;
    }
    remain -= MAX_BYTES;
    in  = MAX_BYTES;
  }

  return true;
}
  
  • Компилятор: gcc 9.1 и выше
  • Флаги компилятора: -fno-inline -O3
  • Ссылка на дизассемблирование Godbolt:https://godbolt.org/z/P8vKGq
  • Ссылка на выполнение программы Godbolt:https://godbolt.org/z/qr8f16

В случае, если я использую -fno-inline флаги компилятора, компилятор пытается оптимизировать приведенный выше код и генерирует только 2 строки кода для my_memcmp функции, однако кажется, что он всегда возвращает 0:

 my_memcmp(void const*, void const*, unsigned long) [clone .constprop.0]:
        movzx   eax, BYTE PTR [rdi]
        ret
  

Проблема не может быть воспроизведена, пока я не добавлю -fno-inline (Я столкнулся с проблемой, когда компилировал код для тестирования покрытия, поэтому мне нужно было добавить no-inline, чтобы сделать отчет более понятным.) Также я обнаружил, что в gcc 8 такой проблемы нет. Есть ли разумное объяснение или это просто ошибка как в GCC 9, так и в 10?

Комментарии:

1. Мое обоснованное предположение заключается в том, что, поскольку компилятор может знать, что это my_memcmp может использоваться только внутри модуля, он может с уверенностью предположить, что единственным вызовом является тот, который в func и оптимизировать my_memcpy на основе этого контекста.

2. @Johan: Да, я думаю, вы правы. Отключение встраивания не отключает межпроцедурную оптимизацию, такую как постоянное распространение в этом случае. Если вы вызываете my_memcmp из другого места с другими аргументами, компилятор сгенерирует отдельную версию функции для этого случая, как здесь . Поэтому я думаю, что это вовсе не ошибка, а довольно умная (и правильная) оптимизация.

3. Компилятор может сказать, что массив обнулен, поэтому он может предположить, что ваш memcmp вернет 0. Чтобы «исправить» это, вы могли бы попробовать рандомизировать значения массива.

4. В конце концов, есть что-то подозрительное. Для заданного значения MAX_BYTES my_memcmp должно вернуться к std::memcmp и, учитывая, что мы ничего не можем знать о in параметре to func , недостаточно просто посмотреть на первый символ.

5. Я заметил, что ваш код в Compiler Explorer MAX_BYTES определен как 256, в отличие от вопроса, который определяет как 9. Пожалуйста, никогда этого не делайте, ваша ссылка на CE должна соответствовать коду в вашем вопросе. Несоответствия вызывают проблемы у людей, пытающихся вам помочь.

Ответ №1:

Это ошибка 95189 GCC,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95189

В принципе, GCC может выдавать специализированный код для memcmp, если один из буферов имеет известное содержимое, но эта специализация не работает корректно, если он встречает нулевой байт (потому что он специальный для других функций, таких как strcmp).

Похоже, что это уже исправлено в основной ветви разработки GCC (магистрали), но исправление еще не было перенесено в ветви выпуска 9.x и 10.x .

Это минимальное воспроизведение в C неправильно скомпилировано в -O2, аналогичный пример упоминается в комментариях к ошибке:

 int f(const char *p)
{
    return __builtin_memcmp(p, "", 4);
}