2

对于 x86 和 x64 编译器生成类似的零/符号扩展 MOVSX 和 MOVZX。扩展本身不是免费的,但允许处理器执行无序的魔法加速。

但在 RISC-V 上:

因此,无符号和有符号 32 位整数之间的转换是无操作的,从有符号 32 位整数到有符号 64 位整数的转换也是如此。

加法和移位需要一些新指令 (ADD[I]W/SUBW/SxxW) 以确保 32 位值的合理性能。

(C) RISC-V 规范

但同时,新的现代 RISC-V 64 位处理器包含 32 位有符号整数的指令。为什么?提高性能?那么 8 位和 16 位在哪里呢?我已经什么都不懂了。

4

3 回答 3

6

完整的报价对我来说似乎很清楚:

编译器和调用约定保持不变,即所有 32 位值都以符号扩展格式保存在 64 位寄存器中。甚至 32 位无符号整数也将第 31 位扩展到第 63 到 32 位。

因此,无符号和有符号 32 位整数之间的转换是无操作的,从有符号 32 位整数到有符号 64 位整数的转换也是如此。
现有的 64 位宽 SLTU 和无符号分支比较仍然在此不变量下对无符号 32 位整数正确运行。
同样,对 32 位符号扩展整数的现有 64 位宽逻辑运算保留符号扩展属性。

加法和移位需要一些新指令 (ADD[I]W/SUBW/SxxW) 以确保 32 位值的合理性能。

它表示 32 位值存储在 64 位寄存器中,其 MSb(最高有效位)通过位 32-63 重复。
这对符号和无符号整数都是如此。

这允许进行一些优化,如报价中所述:

  • 无符号 <-> 有符号转换是免费的。
    将此与通常的算法进行比较,您必须将低 32 位值归零或符号扩展以将其提升为不同“符号”的 64 位值(忽略溢出)。
  • 带符号的 32 位 <-> 带符号的 64 位是免费的。
    这省去了一个符号扩展。
  • 分支和设置指令仍然有效。
    这是因为重复 MSb 不会改变比较结果。
  • 逻辑 64 位操作保留了这个属性
    通过几个例子很容易看出这一点。

然而,加法(仅举一个例子)并没有保留这个不变量: 0x000000007fffffff + 0x0000000000000001 = 0x0000000080000000 这违反了假设。

由于 a) 使用 32 位值经常发生并且 b) 修复结果需要额外的工作(我可以考虑使用slli/srai对),因此引入了一种新的指令格式。
这些指令对 64 位寄存器进行操作,但只使用它们的低 32 位值,并对 32 位结果进行符号扩展。
这很容易在硬件中完成,因此值得拥有这种新的指令。

正如评论中所指出的,8 位和 16 位算术很少见,因此没有花费任何工程精力为其寻找新空间(无论是在所需的门方面还是在使用的操作码空间方面)。

于 2018-10-04T14:00:39.657 回答
4

这是 ABI 开始渗入 ISA 的情况之一。您会在 RISC-V 中找到其中的一些。由于我们在标准化 ISA 时移植了一个相当重要的软件堆栈,因此我们必须微调 ISA 以匹配真实代码。由于基本 RISC-V ISA 的一个明确目标是为未来的扩展保留大量可用的编码空间。

在这种情况下,ABI 设计决定是回答“是否存在类型的规范表示,当存储在寄存器中时,不需要这些寄存器提供的每个位模式来表示该类型可表示的每个值?” 在 RISC-V 的情况下,我们选择为所有类型强制规范表示。这里有一些 ISA 设计决策的反馈循环,我认为解决这个问题的最佳方法是通过一个示例来说明 ISA 与 ABI 共同发展的内容,而我们没有强制要求规范表示。

作为一个思考练习,让我们假设 RISC-V ABI 没有要求对int存储在 RV64I 上的 X 寄存器中的高位进行规范表示。这里的结果是现有的 W 系列指令不会特别有用:您可以使用addiw t0, t0, 0作为符号扩展,因此编译器可以依赖高位中的内容,但这为许多常见模式(如比较+分支)添加了额外的指令。在这里做出的正确 ISA 设计决定是使用一组不同的 W 指令,例如“比较低 32 位和分支”。如果你运行这些数字,你最终会得到大约相同数量的附加指令(分支和设置,而不是加、减和移位)。问题是分支指令在编码空间方面要昂贵得多,因为它们具有更长的偏移量。由于编码空间被认为是 RISC-V 中的重要资源,当没有明显的性能优势时,我们倾向于选择节省更多编码空间的设计决策。在这种情况下,有'

这里有一个二阶设计决策:规范表示是符号扩展还是零扩展?这里有一个权衡:符号扩展导致更快的软件(使用相同数量的编码空间),但更复杂的硬件。具体来说,常见的 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

当我们进行平衡时,似乎零扩展的软件成本高于符号扩展的硬件成本:对于尽可能小的设计(即微编码实现),您仍然需要算术右移,这样您就不会丢失任何数据路径,并且对于最大可能的设计(即,广泛的乱序内核),代码最终会在分支之前将位改组。奇怪的是,您为符号扩展支付有意义的成本的一个地方是具有短流水线的有序机器:您可以从 ALU 路径中减少 MUX 延迟,这在某些设计中至关重要。实际上,在许多其他地方,符号扩展是正确的决定,因此仅更改这一点不会导致删除该数据路径。

于 2018-10-04T20:04:39.717 回答
1

扩展已接受答案的评论,即“8 位和 16 位算术很少见”:一些最常见的计算机语言被设计为不需要它,因为过去流行的 ISA 没有它

C 指定任何比 an 窄的操作数在对其进行任何算术运算时int都会被“提升”到。int在 RISC-V 上,anint是 32 位宽的。从内存中加载它们时,有LB/LBULH/LHU指令可以在零扩展 aunsigned short和符号扩展 a之间进行选择。signed char

除此之外,C 系列语言不需要任何对 8 位或 16 位数学的支持。对于像这样的常见情况some_unsigned_short += 1,有某种ADDIH自动截断结果的假​​设可能会有所帮助。然而,这只是一个额外的指令(位掩码由0xFFFF)。像这样的表达式some_signed_short -= 1甚至不需要做那么多就可以“正确”,或者至少他们的编译器在技术上符合语言标准,因为有符号的溢出或下溢在 C 中是未定义的行为,所以编译器可以忽略这种可能性或为所欲为。

于 2019-07-31T18:34:09.850 回答