Ответ 1
Если вы посмотрите на различные команды умножения x86, глядя только на 32-битные варианты и игнорируя BMI2, вы найдете их:
-
imul r/m32
(умноженное на 32x32- > 64) -
imul r32, r/m32
(32x32- > 32 умножить) * -
imul r32, r/m32, imm
(32x32- > 32 умножить) * -
mul r/m32
(32x32- > 64 беззнакового умножения)
Обратите внимание, что только "расширяющееся" умножение имеет неподписанный аналог. Две формы в середине, отмеченные звездочкой, являются как подписанными, так и беззнаковыми умножениями, потому что для случая, когда вы не получаете эту дополнительную "верхнюю часть", это то же самое.
"Расширяющиеся" умножения не имеют прямого эквивалента в C, но компиляторы могут (и часто делают) использовать эти формы в любом случае.
Например, если вы скомпилируете это:
uint32_t test(uint32_t a, uint32_t b)
{
return a * b;
}
int32_t test(int32_t a, int32_t b)
{
return a * b;
}
С GCC или некоторым другим относительно разумным компилятором вы получите что-то вроде этого:
test(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
test(int, int):
mov eax, edi
imul eax, esi
ret
(фактический вывод GCC с -O1)
Таким образом, подпись не имеет значения для умножения (по крайней мере, не для типа умножения, которое вы используете в C) и для некоторых других операций, а именно:
- сложение и вычитание
- побитовое И, ИЛИ, XOR, NOT
- Отрицание
- сдвиг влево
- сравнение для равенства
x86 не предлагает отдельные подписанные/неподписанные версии для них, поскольку в этом нет никакой разницы.
Но для некоторых операций существует разница, например:
- деление (
idiv
vsdiv
) - остаток (также
idiv
vsdiv
) - правый сдвиг (
sar
vsshr
) (но остерегайтесь подписанного сдвига справа в C) - для сравнения больше/меньше, чем
Но последний является особенным, x86 не имеет отдельных версий для подписанных и неподписанных, либо он имеет одну операцию (cmp
, которая на самом деле просто неразрушающая sub
), которая делает это одновременно, и дает несколько результатов (затронуты несколько бит в "флажках" ). Более поздние инструкции, которые фактически используют эти флаги (ветки, условные перемещения, setcc
), затем выбирают, какие флаги им нужны. Так, например,
cmp a, b
jg somewhere
Пойдет somewhere
, если a
"подписано больше" b
.
cmp a, b
jb somewhere
Пошел бы somewhere
, если a
"без знака ниже" b
.
См. Assembly - JG/JNLE/JL/JNGE после CMP для получения дополнительных сведений о флажках и ветвях.
Это не будет формальным доказательством того, что подписанное и беззнаковое умножение одно и то же, я просто попытаюсь дать вам представление о том, почему они должны быть одинаковыми.
Рассмотрим 4-битные целые числа. Вес их отдельных битов составляет от lsb до msb, 1, 2, 4 и -8. Когда вы умножаете два из этих чисел, вы можете разложить один из них на 4 части, соответствующие его битам, например:
0011 (decompose this one to keep it interesting)
0010
---- *
0010 (from the bit with weight 1)
0100 (from the bit with weight 2, so shifted left 1)
---- +
0110
2 * 3 = 6, поэтому все проверяется. Это просто регулярное многократное умножение, которое большинство людей учит в школе, только двоичное, что делает его намного проще, поскольку вам не нужно умножаться на десятичную цифру, вам нужно только умножить на 0 или 1 и сдвинуть.
В любом случае, теперь возьмите отрицательное число. Вес знакового бита равен -8, поэтому в какой-то момент вы сделаете частичный продукт -8 * something
. Умножение на 8 сдвигается налево на 3, поэтому прежний lsb теперь является msb, а все остальные бит равны 0. Теперь, если вы отрицаете это (это было -8 в конце концов, а не 8), ничего не происходит. Очевидно, что Zero неизменен, но так же, как и 8, и вообще число с только набором msb:
-1000 = ~1000 + 1 = 0111 + 1 = 1000
Итак, вы сделали то же самое, что и сделали бы, если бы вес msb был 8 (как в случае без знака) вместо -8.