Оптимизация компилятора побитовой не операции

У меня есть простое тестирование функции, если два массива обращены друг к другу. Они кажутся идентичными, за исключением переменной tmp. Один работает, другой нет. Я не могу понять, почему компилятор оптимизировал бы это - если это действительно проблема оптимизации (мой компилятор - IAR Workbench v4.30.1). Вот мой код:

// this works as expected
uint8 verifyInverseBuffer(uint8 *buf, uint8 *bufi, uint32 len)
{
  uint8 tmp;
  for (uint32 i = 0; i < len; i++)
  {
    tmp = ~bufi[i];
    if (buf[i] != tmp)
    {
      return 0;
    }
  }
  return 1;  
}

// this does NOT work as expected (I only removed the tmp!)
uint8 verifyInverseBuffer(uint8 *buf, uint8 *bufi, uint32 len)
{
  for (uint32 i = 0; i < len; i++)
  {
    if (buf[i] != (~bufi[i]))
    {
      return 0;
    }
  }
  return 1;  
}

Первая версия кода работает, вторая - нет. Кто-нибудь может понять, почему? Или прийти с некоторыми тестами, чтобы выяснить, что не так?

Ответы

Ответ 1

То, что вы видите, происходит в результате правил целочисленных рекламных акций. Каждый раз, когда в выражении используется переменная, меньшая, чем int, значение переводится в тип int.

Предположим, что bufi[i] содержит значение 255. Шестнадцатеричное представление этого значения - 0xFF. Затем это значение является операндом оператора ~. Таким образом, значение будет сначала повышено до int, которое (при условии, что оно 32-разрядное) будет иметь значение 0x000000FF, и применение ~ к этому даст вам 0xFFFFFF00. Затем вы сравниваете это значение с buf[i], который имеет тип uint8_t. Значение 0xFFFFFF00 находится вне этого диапазона, поэтому сравнение всегда будет ложным.

Если вы присваиваете результат ~ обратно переменной типа uint8_t, значение 0xFFFFFF00 преобразуется в 0x00. Именно это преобразованное значение затем сравнивается с buf[i].

Таким образом, поведение, которое вы видите, является не результатом оптимизации, а правилами языка. Использование временной переменной как вы есть один из способов решения этой проблемы. Вы также можете привести результат к uint8:

if(buf[i] != (uint8)(~bufi[i]))

Или замаскируйте все, кроме младшего байта:

if(buf[i] != (~bufi[i] & 0xff))

Ответ 2

Проблема в целочисленном продвижении. Оператор ~ очень опасен!

В случае ~bufi[i] операнд из ~ повышается в соответствии с целочисленными продвижениями. Создание кода, эквивалентного ~(int)bufi[i].

Таким образом, во втором случае buf[i] != (~bufi[i]) вы получите что-то вроде 0xXX != 0xFFFFFFFFYY, где "XX" и "YY" - это фактические значения, которые вы хотите сравнить, а 0xFFFF - это непреднамеренное дерьмо, помещенное туда с помощью побитового дополнения int. Это всегда будет соответствовать true, так что компилятор может оптимизировать отдельные части кода, создав очень тонкую ошибку.

В случае tmp = ~bufi[i]; вы уклоняетесь от этой ошибки, усекая 0xFFFFFFFFYY в "YY", значение, которое вас интересует.

Подробнее см. Правила продвижения неявных типов. Также подумайте о том, чтобы принять MISRA-C, чтобы избежать подобных ошибок.

Ответ 3

Как уже отмечали Lundin и dbush, сравнение во второй версии всегда терпит неудачу, потому что противоположность любого значения uint8, повышенного до int, отличается от всех значений uint8. Другими словами, вторая версия эквивалентна:

// this does NOT work as expected (I only removed the tmp!)
uint8 verifyInverseBuffer(uint8 *buf, uint8 *bufi, uint32 len) {
    if (len) return 0;
    return 1;
}

Как видно из проводника компилятора Godbolt, gcc и clang обнаруживают это и полностью оптимизируют код:

verifyInverseBuffer:
    test    edx, edx
    sete    al
    ret

gcc выдает довольно загадочное предупреждение, указывающее на подозрительную проблему сравнения подписанных/неподписанных, которая не является реальной проблемой... Закрыть, но не банан.

<source>: In function 'verifyInverseBuffer':
<source>:8:16: warning: comparison of promoted bitwise complement of an unsigned value with unsigned [-Wsign-compare]
    8 |     if (buf[i] != (~bufi[i]))
      |                ^~
Compiler returned: 0