Почему (% 256) отличается от (a & 0xFF)?

145

Я всегда предполагал, что при выполнении (a % 256)оптимизатора, естественно, будет использоваться эффективная побитовая операция, как если бы я писал (a & 0xFF).

При тестировании на проводнике компилятора gcc-6.2 (-O3):

// Type your code here, or load an example.
int mod(int num) {
    return num % 256;
}

mod(int):
    mov     edx, edi
    sar     edx, 31
    shr     edx, 24
    lea     eax, [rdi+rdx]
    movzx   eax, al
    sub     eax, edx
    ret

И при попытке другого кода:

// Type your code here, or load an example.
int mod(int num) {
    return num & 0xFF;
}

mod(int):
    movzx   eax, dil
    ret

Кажется, я что-то упустил. Любые идеи?

Элад Вайс
источник
64
0xFF - 255, а не 256.
Ришикеш Радж
186
@RishikeshRaje: Так? %не &либо.
usr2564301
27
@RishikeshRaje: Я уверен, что ОП очень хорошо знает об этом. Они используются с различными операциями.
ура и hth. - Альф
28
Из интереса, вы получите лучшие результаты, если numесть unsigned?
Вирсавия
20
@RishikeshRaje Побитовое и 0xFF эквивалентно модулю 2 ^ 8 для целых чисел без знака.
2501

Ответы:

230

Это не одно и то же. Попробуйте num = -79, и вы получите разные результаты от обеих операций. (-79) % 256 = -79, а (-79) & 0xffэто какое-то положительное число.

Используя unsigned int, операции одинаковы, и код, скорее всего, будет таким же.

PS- Кто-то прокомментировал

Они не должны быть одинаковыми, a % bопределяется как a - b * floor (a / b).

Это не так, как это определено в C, C ++, Objective-C (т.е. во всех языках, где код в вопросе будет компилироваться).

gnasher729
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Мартин Питерс
52

Короткий ответ

-1 % 256урожайность -1а не 255какая есть -1 & 0xFF. Поэтому оптимизация будет некорректной.

Длинный ответ

C ++ имеет соглашение (a/b)*b + a%b == a, которое кажется вполне естественным. a/bвсегда возвращает арифметический результат без дробной части (усечение до 0). Как следствие, a%bимеет тот же знак, что aи 0.

Деление -1/256дает 0и, следовательно, -1%256должно быть -1для удовлетворения вышеуказанного условия ( (-1%256)*256 + -1%256 == -1). Это явно отличается от того, -1&0xFFчто есть 0xFF. Поэтому компилятор не может оптимизировать так, как вы хотите.

Соответствующий раздел в стандарте C ++ [expr.mul §4] от N4606 гласит:

Для целочисленных операндов /оператор дает алгебраический фактор с любой отброшенной дробной частью; если частное a/bпредставимо в типе результата, (a/b)*b + a%bравно a[...].

Включение оптимизации

Однако, используя unsignedтипы, оптимизация будет полностью правильной , удовлетворяя вышеуказанному соглашению:

unsigned(-1)%256 == 0xFF

Смотрите также это .

Другие языки

Это обрабатывается очень по-разному в разных языках программирования, как вы можете посмотреть в Википедии .

Ральф Тандецки
источник
50

Начиная с C ++ 11, num % 256должен быть неположительным, если numотрицательным.

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

Было бы другое дело, если бы numв вашем случае это было unsigned: в эти дни я почти ожидал, что компилятор сделает оптимизацию, которую вы цитируете.

Вирсавия
источник
6
Почти, но не совсем. Если numотрицательно, то num % 256равно нулю или отрицательно (иначе не положительно).
Наюки
5
Какой ИМО, является ошибкой в ​​стандарте: математически по модулю операция должна принимать знак делителя, 256 в этом случае. Чтобы понять, почему считают это (-250+256)%256==6, но (-250%256)+(256%256)должно быть, согласно стандарту, «неположительным», и, следовательно, нет 6. Такое нарушение ассоциативности имеет реальные побочные эффекты: например, при вычислении рендеринга "уменьшения масштаба" в целочисленных координатах необходимо сместить изображение, чтобы все координаты были неотрицательными.
Майкл
2
@Michael Modulus никогда не был дистрибутивным по сложению («ассоциативное» - неправильное имя для этого свойства!), Даже если вы следите за математическим определением буквы. Например, (128+128)%256==0но (128%256)+(128%256)==256. Возможно, есть хорошее возражение против указанного поведения, но мне не ясно, что это то, что вы сказали.
Даниэль Вагнер
1
@DanielWagner, вы правы, конечно, я ошибся с «ассоциативным». Однако, если сохранить знак делителя и вычислить все в модульной арифметике, свойство распределения действительно имеет место; в вашем примере вы бы имели 256==0. Ключ должен иметь точно Nвозможные значения по модулю Nарифметики, что возможно, только если все результаты находятся в диапазоне 0,...,(N-1), а не -(N-1),...,(N-1).
Майкл
6
@Michael: За исключением того, что% не является оператором по модулю, это оператор остатка .
Joren
11

У меня нет телепатического понимания рассуждений компилятора, но в случае необходимости %приходится иметь дело с отрицательными значениями (и делением округляется до нуля), в то время &как результатом всегда являются младшие 8 бит.

Эти sarзвуки команд мне , как «сдвиг арифметического права», заполняя освободившиеся биты со знаком битового значения.

Ура и hth. Альф
источник
0

Говоря математически, по модулю определяется следующее:

% b = a - b * этаж (a / b)

Это прямо здесь должно прояснить это для вас. Мы можем исключить floor для целых чисел, потому что целочисленное деление эквивалентно floor (a / b). Однако, если бы компилятор использовал общий трюк, как вы предлагаете, он должен был бы работать для всех a и всех b. К сожалению, это просто не тот случай. Говоря математически, ваш трюк на 100% правильный для целых чисел без знака (я вижу, что состояния ответа знаковые целые числа ломаются, но я могу подтвердить или опровергнуть это, так как -a% b должен быть положительным). Тем не менее, вы можете сделать этот трюк для всех б? Возможно нет. Вот почему компилятор этого не делает. В конце концов, если бы модуль был легко записан как одна побитовая операция, то мы просто добавили бы схему по модулю, как для сложения и других операций.

user64742
источник
4
Я думаю, что вы путаете слово "пол" с словом "усечь". В ранних компьютерах использовалось усеченное деление, потому что вычисление зачастую проще, чем по полам, даже в тех случаях, когда дела делятся равномерно. Я видел очень мало случаев, когда усеченное деление было более полезным, чем деление по этажам, но многие языки следуют руководству FORTRAN по использованию усеченного деления.
суперкат
@supercat Математически говоря, пол является усеченным. Они оба имеют одинаковый эффект. Они не могут быть реализованы одинаково на компьютере, но они делают то же самое.
user64742 10.11.16
5
@TheGreatDuck: они не одинаковы для отрицательных чисел. Пол -2.3является -3, в то время как если вы усечь -2.3до целого числа вы получите -2. См. En.wikipedia.org/wiki/Truncation . msgstr "усечение отрицательных чисел не округляется в том же направлении, что и функция пола". И поведение %отрицательных чисел как раз и является причиной того, что ОП видит описанное поведение.
Марк Дикинсон
@MarkDickinson Я почти уверен, что по модулю в c ++ положительные значения для положительных делителей дают положительные значения, но я не буду спорить.
user64742 10.11.16
1
@TheGreatDuck - см. Пример: cpp.sh/3g7h (Обратите внимание, что C ++ 98 не определил, какой из двух возможных вариантов был использован, но что более современные стандарты позволяют , поэтому возможно, что вы использовали реализацию C ++ в прошлом, которые делали это по-другому ...)
Периата Breatta