Оптимизация компилятора побитовой не операции
У меня есть простое тестирование функции, если два массива обращены друг к другу.
Они кажутся идентичными, за исключением переменной 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