Ноль / знак-расширение не нужны, зачем тогда инструкции для каждого типа размера?
Для x86 и x64 компиляторы генерируют одинаковое расширение ноль / знак MOVSX и MOVZX. Само по себе расширение не является бесплатным, но позволяет процессорам ускорять магические операции вне очереди.
Но на RISC-V:
Следовательно, преобразование между 32-разрядными целыми числами без знака и со знаком не допускается, равно как и преобразование 32-разрядного целого числа со знаком в 64-разрядное целое число со знаком.
Несколько новых инструкций (ADD[I]W/SUBW/SxxW) требуются для сложения и сдвигов, чтобы обеспечить приемлемую производительность для 32-битных значений.
(C) RISC-V Spec
Но в то же время новые современные 64-разрядные процессоры RISC-V содержат инструкции для 32-разрядных целых чисел со знаком. Зачем? Чтобы увеличить производительность? Где тогда 8 и 16 бит? Я уже ничего не понимаю.
2 ответа
Полная цитата кажется мне понятной:
Соглашение о компиляторе и вызовах поддерживает инвариант, согласно которому все 32-разрядные значения хранятся в расширенном знаковом формате в 64-разрядных регистрах. Даже 32-разрядные целые числа без знака расширяют бит 31 на биты с 63 по 32.
Следовательно, преобразование между 32-разрядными целыми числами без знака и со знаком не допускается, равно как и преобразование 32-разрядного целого числа со знаком в 64-разрядное целое число со знаком.
Существующие 64-битные SLTU и сравнения беззнаковых ветвей по-прежнему корректно работают с 32-битными целыми без знака в соответствии с этим инвариантом.
Точно так же существующие 64-битные логические операции над 32-битными целыми числами с расширенным знаком сохраняют свойство расширения знака.Несколько новых инструкций (ADD[I]W/SUBW/SxxW) требуются для сложения и сдвигов, чтобы обеспечить приемлемую производительность для 32-битных значений.
В нем говорится, что 32-битные значения хранятся в 64-битных регистрах, а их MSb (старший бит) повторяется через биты 32-63.
Это делается для целых чисел со знаком и без знака.
Это позволяет несколько оптимизаций, как указано в цитате:
- Без подписи <-> подписанное преобразование бесплатно.
Сравните это с обычным алгоритмом, в котором необходимо обнулить или подписать расширение нижнего 32-разрядного значения, чтобы преобразовать его в 64-разрядное значение различной "значимости" (игнорирование переполнения). - Подписанный 32-разрядный <-> Подписанный 64-разрядный свободен.
Это лишает знак расширения. - Ветки и инструкции по установке все еще работают.
Это потому, что повторение MSb не меняет результат сравнения. - Логические 64-битные операции сохраняют это свойство
Это легко увидеть после нескольких примеров.
Однако дополнение (чтобы назвать его) не сохраняет этот инвариант: 0x000000007fffffff + 0x0000000000000001 = 0x0000000080000000, что нарушает предположение.
Поскольку а) работа с 32-битными значениями происходит очень часто и б) исправление результата потребует дополнительной работы (я могу подумать об использовании slli
/srai
пара) введен новый формат инструкций.
Эти инструкции работают с 64-битными регистрами, но используют только их более низкое 32-битное значение и будут расширять 32-битный результат.
Это легко сделать аппаратно, поэтому стоит иметь этот новый класс обучения.
Как отмечается в комментариях, 8- и 16-разрядная арифметика встречается редко, поэтому не было затрачено никаких технических усилий на поиск места для нее (как с точки зрения требуемых вентилей, так и используемого пространства кода операции).
Это один из тех случаев, когда ABI начинает кровоточить в ISA. Вы найдете несколько таких в RISC-V. В результате того, что у нас был довольно значительный программный стек, портированный к моменту стандартизации ISA, мы получили возможность точно настроить ISA для соответствия реальному коду. Поскольку явная цель базовых ISA RISC-V состояла в том, чтобы сохранить много места для кодирования, доступного для будущего расширения.
В этом случае проектное решение ABI состоит в том, чтобы ответить на вопрос "Существует ли каноническое представление типов, которым при хранении в регистрах не требуется каждый битовый шаблон, предоставленный этими регистрами, для представления каждого значения, представляемого типом?" В случае RISC-V мы решили назначить каноническое представление для всех типов. Здесь есть петля обратной связи с некоторыми проектными решениями ISA, и я думаю, что лучший способ сделать это - проработать пример того, что ISA развивалось бы совместно с ABI, где мы не требовали канонического представления.
В качестве упражнения для размышления предположим, что RISC-V ABI не предписывал каноническое представление для старших битов int
при сохранении в регистре X на RV64I. В результате получается, что существующее семейство инструкций W не будет особенно полезным: вы можете использовать addiw t0, t0, 0
как расширение знака, так что компилятор может полагаться на то, что находится в старших битах, но это добавляет дополнительную инструкцию ко многим распространенным шаблонам, таким как сравнение + ветвь. Правильное проектное решение ISA, которое нужно принять здесь, будет иметь другой набор W-инструкций, что-то вроде "сравните на младших 32 битах и ветви". Если вы запустите числа, вы получите примерно такое же количество дополнительных инструкций (ветвление и установка в отличие от сложения, подстановки и сдвига). Проблема в том, что инструкции ветвления намного дороже с точки зрения пространства кодирования, потому что они имеют гораздо более длинные смещения. Поскольку пространство кодирования считается важным ресурсом в RISC-V, когда нет явного преимущества в производительности, мы склонны выбирать проектное решение, которое экономит больше места для кодирования. В этом случае нет значимого различия в производительности, если ABI соответствует ISA.
Здесь следует принять решение о разработке второго порядка: является ли каноническое представление расширением знака или расширением нуля? Здесь есть компромисс: расширение знака приводит к более быстрому программному обеспечению (при том же объеме используемого пространства кодирования), но более сложному аппаратному обеспечению. В частности, общий фрагмент C
long func_pos();
long func_neg();
long neg_or_pos(int a) {
if (a > 0) return func_pos();
return func_neg();
}
очень эффективно компилируется при использовании расширения знака
neg_or_pos:
bgtz a0,.L4
tail func_neg
.L4:
tail func_pos
но медленнее, когда используется нулевое расширение (опять же, при условии, что мы не хотим использовать много места для кодирования в инструкциях сравнения + ветвления размера слова)
neg_or_pos:
addiw a0, a0, 0
bgtz a0,.L4
tail func_neg
.L4:
tail func_pos
Когда мы уравновесили ситуацию, оказалось, что стоимость программного обеспечения с нулевым расширением выше, чем стоимость оборудования с расширением знака: для наименьшего возможного дизайна (т. Е. Микрокодированной реализации) вам все еще нужен арифметический сдвиг вправо, чтобы не потерять любой путь к данным, и для максимально возможного дизайна (т. е. с широким ядром, вышедшим из строя) код просто заканчивал бы перемешиванием битов перед переходом. Как ни странно, единственное место, где вы платите значительную цену за расширение знака, - это машины на заказ с короткими конвейерами: вы могли бы сократить задержку MUX на пути ALU, что является критическим в некоторых конструкциях. На практике есть много других мест, где расширение знака является правильным решением, поэтому простое изменение этого не приведет к удалению этого канала данных.
To expand on the accepted answer’s comment that “8 and 16-bit arithmetic is rare”: some of the most common computer languages are designed not to need it, because popular ISAs of the past did not have it
C specifies that any operand narrower than an int
gets “promoted” to int
when doing any arithmetic on it. On RISC-V, an int
is 32-bits wide. There are the LB
/LBU
and LH
/LHU
instructions to choose between zero-extending an unsigned short
and sign extending a signed char
when loading them from memory.
C-family languages don’t need any support for 8-bit or 16-bit math beyond that. For common cases like some_unsigned_short += 1
, it might be somewhat useful to have some kind of hypothetical ADDIH
that automatically truncates the result. However, that’s just one extra instruction (bitmask by 0xFFFF
). Expressions like some_signed_short -= 1
don’t even need to do that much to be “correct,” or at least for their compilers to technically comply with the language Standard, because signed overflow or underflow is undefined behavior in C, so the compiler can just ignore the possibility or do whatever it wants.