Какие целые операции с дополнением 2 можно использовать без обнуления высоких бит в входах, если требуется только низкая часть результата?

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

Это особенно характерно для x86-64 (aka AMD64) по разным причинам 1 , некоторые из которых присутствуют в других ISA.

Я буду использовать 64-битные x86 для примеров, но цель состоит в том, чтобы спросить о / обсуждать дополнение 2 и двоичную арифметику без знака в целом, поскольку все современные процессоры используют ее . (Обратите внимание, что C и C ++ не гарантируют два дополнения 4 , а подписанное переполнение – неопределенное поведение.)

В качестве примера рассмотрим простую функцию, которая может компилироваться в инструкцию LEA 2 . (В x86-64 SysV (Linux) ABI 3 первые два аргумента функции находятся в rdi и rsi с возвратом в rax . int – 32-битный тип.)

 ; int intfunc(int a, int b) { return a + b*4 + 3; } intfunc: lea eax, [edi + esi*4 + 3] ; the obvious choice, but gcc can do better ret 

gcc знает, что добавление, даже отрицательных целых чисел, переносится только справа налево, поэтому верхние биты входов не могут влиять на то, что входит в eax . Таким образом, он сохраняет байты команд и использует lea eax, [rdi + rsi*4 + 3]

Какие другие операции имеют это свойство для младших бит результата, не зависящего от старших бит входов?

И почему это работает?



Сноски

1 Почему это часто возникает для x86-64 : x86-64 имеет инструкции переменной длины, где дополнительный префиксный байт изменяет размер операнда (от 32 до 64 или 16), поэтому сохранение байта часто возможно в инструкциях, которые в противном случае выполненных с той же скоростью. Он также имеет ложные зависимости (AMD / P4 / Silvermont) при записи низких 8b или 16b регистра (или стоп, когда позже читает полный регистр (Intel pre-IvB)): по историческим причинам только запись на 32b sub -регирует ноль остальную часть регистра 64b . Почти все арифметики и логики могут использоваться на низких 8, 16 или 32 битах, а также на всех 64 битах регистров общего назначения. Целочисленные векторные инструкции также довольно не ортогональны, некоторые операции недоступны для некоторых размеров элементов.

Кроме того, в отличие от x86-32, ABI передает функции args в регистры, а верхние биты не обязательно равны нулю для узких типов.

2 LEA: Как и другие инструкции, размер операнда по умолчанию LEA составляет 32 бит, но размер адреса по умолчанию составляет 64 бит. Байт префикса размера операнда ( 0x66 или REX.W ) может сделать размер операнда 16 или 64 бит. Байт префикса размера адреса ( 0x67 ) может уменьшить размер адреса до 32 бит (в режиме 64 бит) или 16 бит (в 32-битном режиме). Поэтому в 64-битном режиме lea eax, [edx+esi] занимает один байт больше, чем lea eax, [rdx+rsi] .

Можно выполнить lea rax, [edx+esi] , но адрес по-прежнему вычисляется только с 32 бит (перенос не устанавливает бит 32 rax ). Вы получаете идентичные результаты с lea eax, [rdx+rsi] , который на два байта короче. Таким образом, префикс размера адреса никогда не будет полезен с LEA , так как комментарии в parsingке выводятся из превосходного демонстратора objconv от Agner Fog.

3 x86 ABI : вызывающему абоненту не нужно иметь нуль (или подписывать-расширять) верхнюю часть 64-битных регистров, используемых для передачи или возврата меньших типов по значению. Вызывающий, который хотел использовать возвращаемое значение в качестве индекса массива, должен был бы movzx rax, eax его (с помощью movzx rax, eax или команды cdqe для специального случая для eax (не путать с cdq , расширяет eax в edx:eax например, для установки idiv .))

Это означает, что функция, возвращающая unsigned int может вычислить возвращаемое значение в 64-битном временном режиме в rax и не требует, чтобы mov eax, eax чтобы обнулить верхние биты rax . Это конструктивное решение работает в большинстве случаев: часто вызывающему абоненту не нужны какие-либо дополнительные инструкции, чтобы игнорировать неопределенные биты в верхней половине rax .


4 C и C ++

C и C ++, в частности, не требуют двоичных двоичных целых двоичных дополнений (кроме C ++ std::atomic types ). Также допускаются дополнения и знак / величина , поэтому для полностью переносимого C эти трюки полезны только для unsigned типов. Очевидно, что для подписанных операций набор знака-бита в представлении знака / величины означает, что другие биты вычитаются, а не добавляются, например. Я не проработал логику для своего дополнения

Однако бит-хаки, которые работают только с дополнением двух, широко распространены , потому что на практике никто не заботится ни о чем другом. Многие вещи, которые работают с дополнением двух, также должны работать со своим дополнением, поскольку бит знака по-прежнему не меняет интерпретации других битов: он просто имеет значение – (2 N -1) (вместо 2 N ). Знак / величина представления не имеет этого свойства: значение места каждого бита положительное или отрицательное в зависимости от знакового бита.

Также обратите внимание, что компиляторам C разрешено считать, что подписанное переполнение никогда не происходит , потому что это неопределенное поведение. Поэтому, например, компиляторы могут и предполагают (x+1) < x всегда false . Это делает обнаружение подписанного переполнения довольно неудобным в C. Обратите внимание, что разница между unsigned wraparound (carry) и подписанным переполнением .

Широкие операции, которые могут использоваться с мусором в верхних битах:

  • побитовые логики
  • левый сдвиг (включая *scale в [reg1 + reg2*scale + disp] )
  • сложение / вычитание (и, таким образом, инструкции LEA : префикс размера адреса никогда не понадобится. Просто используйте требуемый размер операнда, чтобы обрезать, если необходимо.)
  • Низкая половина умножается. например 16b x 16b -> 16b, можно сделать с 32b x 32b -> 32b. Вы можете избежать LCP-ларьков (и проблем с частичным регистром) из imul r16, r/m16, imm16 , используя 32- imul r32, r/m32, imm32 и затем считывая только низкий результат 16. (Будьте осторожны с более подробной памятью, если используете версию m32 .)

    Как указано в инструкции Intel insn ref, 2 и 3 формы операнда imul безопасны для использования в целых числах без знака. Знаковые биты входов не влияют на N бит результата в бит N x N -> N бит.)

  • 2 x (т. Е. Сдвиг по x ): работает как минимум на x86, где количество сдвигов маскируется, а не насыщается, вплоть до ширины операции, поэтому высокий мусор в ecx или даже высокие бит cl , t влияет на количество сдвигов. Также применяется к беспошлинным сдвигам BMI2 ( shlx т. Д.), Но не к векторным сдвигам ( pslld xmm, xmm/m128 т. Д., pslld xmm, xmm/m128 насыщают счет). Интеллектуальные компиляторы оптимизируют маскировку счета сдвига, позволяя безопасную идиому для вращения в C (нет неопределенного поведения) .

Очевидно, что флаги, такие как carry / overflow / sign / zero, будут зависеть от мусора в высоких бит более широкой операции. Сдвиги x86 помещают последний бит, смещенный в флаг переноса, так что это даже влияет на сдвиги.

Операции, которые нельзя использовать с мусором в верхних битах:

  • правая смена
  • полное умножение: например, для 16b x 16b -> 32b, убедитесь, что верхние 16 входов имеют нуль или расширенный знак перед выполнением 32b x 32b -> 32b imul . Или используйте 16-битный один-операнд mul или imul чтобы неудобно поместить результат в dx:ax . (Выбор подписанной или неподписанной команды будет влиять на верхний 16b так же, как ноль или знак, до 32b imul .)

  • адресация памяти ( [rsi + rax] ): знак или нуль расширяются по мере необходимости. Нет режима адресации [rsi + eax] .

  • разделение и остаток

  • log2 (т. е. положение самого старшего бита)
  • завершение нулевого отсчета (если вы не знаете, что бит бит находится где-то в той части, которую вы хотите, или просто проверьте результат больше, чем N, поскольку вы не обнаружили проверку.)

Дополнением 2, как беззнаковое основание 2, является система ценностей места. MSB для unsigned base2 имеет значение места 2 N-1 в N битовом числе (например, 2 31 ). В дополнении 2 MSB имеет значение -2 N-1 (и, следовательно, работает как знаковый бит). В статье wikipedia объясняется много других способов понимания дополнения 2 и отрицания номера без знака base2.

Ключевым моментом является то , что наличие установленного битового знака не меняет интерпретации других битов . Сложение и вычитание работают точно так же, как для unsigned base2, и только интерпретация результата отличается между подписанным и unsigned. (например, подписанное переполнение происходит, когда есть перенос, но не из знакового бита ).

Кроме того, перенос распространяется только от LSB до MSB (справа налево). Вычитание одинаково: независимо от того, есть ли что-либо в высоких бит для заимствования, низкие бит заимствуют его. Если это вызывает переполнение или перенос, будут затронуты только высокие биты. например:

  0x801F -0x9123 ------- 0xeefc 

Низкие 8 бит, 0xFC , не зависят от того, что они заимствовали. Они «обертываются» и передают заимствование в верхние 8.

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

Поскольку LEA использует только добавление (и сдвиг влево), использование размера адреса по умолчанию всегда прекрасное. Отсрочка усечения до тех пор, пока размер операнда не вступит в игру, результат всегда прекрасен.

(Исключение: 16-битный код может использовать префикс размера адреса для выполнения 32-битной математики. В коде 32 или 64b префикс размера адреса уменьшает ширину вместо увеличения.)


Умножение можно рассматривать как повторное добавление, или как смещение и добавление. На нижнюю половину не влияют никакие верхние биты. В этом 4-битном примере я выписал все бит-продукты, которые суммируются в биты с низким результатом. Используются только два младших бита обоих источников. Понятно, что это работает в целом: частичные продукты сдвигаются перед добавлением, поэтому высокие бит в источнике никогда не влияют на младшие биты в результате в целом.

См. Википедию для более широкой версии этого с гораздо более подробным объяснением . Есть много хороших хитов google для бинарного умножения , включая некоторые учебные материалы.

  *Warning*: This diagram is probably slightly bogus. ABCD A has a place value of -2^3 = -8 * abcd a has a place value of -2^3 = -8 ------ RRRRrrrr AAAAABCD * d sign-extended partial products + AAAABCD * c + AAABCD * b - AABCD * a (a * A = +2^6, since the negatives cancel) ---------- D*d ^ C*d+D*c 

Выполнение подписанного множителя, а не беззнакового умножения, все равно дает тот же результат в нижней половине (низкие 4 бита в этом примере). Расширение знаков частичных продуктов происходит только в верхней половине результата.

Это объяснение не очень тщательное (и, возможно, даже имеет ошибки), но есть веские доказательства того, что это правда и безопасно использовать в производственном коде:

  • gcc использует imul для вычисления unsigned long произведения двух unsigned long входов. См. Пример этого gcc, использующего LEA для других функций в проводнике компилятора Godbolt .

  • Руководство Intel по insn ref говорит:

Двух- и трехоперационные формы также могут использоваться с неподписанными операндами, потому что нижняя половина продукта одинакова независимо от того, подписаны ли операнды или нет. Однако флаги CF и OF не могут использоваться, чтобы определить, отлична ли верхняя половина результата.

  • Проектное решение Intel вводит только 2 и 3 операндных формы imul , а не mul .

Очевидно, что побитовые двоичные логические операции (и / или / xor / not) обрабатывают каждый бит независимо: результат для позиции бита зависит только от значения входных данных в этой позиции бита. Бит-сдвиги также довольно очевидны.

  • Что делает NOPL в системе x86?
  • Почему x86 маленький endian?
  • Почему XCHG reg, reg 3 инструкции по микрооперации на современных архитектурах Intel?
  • Разница между JA и JG в сборке
  • Что такое 0x10 в инструкции по сборке «leal 0x10 (% ebx),% eax» x86?
  • Что означает «DS: » означает в сборке?
  • Проблемы с ADC / SBB и INC / DEC в узких петлях на некоторых процессорах
  • Как вы используете gcc для генерации кода сборки в синтаксисе Intel?
  • Ассемблер ADC (добавить с переносом) в C ++
  • Может ли x86's MOV быть «свободным»? Почему я не могу воспроизвести это вообще?
  • 16-разрядные режимы адресации NASM x86
  • Interesting Posts

    Пиксельный экран, сопровождаемый жестким сбоем

    Как распечатать любой документ / изображение в файл?

    Отобразите «Мои документы» / «Мои видео» / и т. Д. Без префикса «Мой» в Windows 7

    Как узнать, когда создавать интерфейс?

    Как написать собственный конвертер для

    Автоматизированные тесты для Java Swing GUI

    Создание веб-запроса на веб-страницу, для которой требуется проверка подлинности Windows

    Как использовать TimeZoneInfo для получения местного времени во время летнего времени?

    Драйверы графической подсистемы Intel – проблемы с оттенком?

    Что со знаком доллара ($ “строка”)

    Перемещение файлов из одного места в другое во время их использования

    Эффективно находим пересечение переменного числа множеств строк

    Чтение встроенного XML-файла c #

    Неверный class или магия файла

    Как переопределить CSS PrimeFaces по умолчанию с пользовательскими стилями?

    Давайте будем гением компьютера.