零/符号扩展是没有操作的,为什么那么说明每种大小类型



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

但在RISC-V上:

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

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

(C) RISC-V规范

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

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

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

因此在无符号和有符号的32位整数之间的转换是无运算的,从有符号的32位整数到有符号的64位整数的转换也是如此
现有64位宽SLTU和无符号分支比较在这个不变量
类似地,对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位算术是罕见的,因此没有花费任何工程精力为其寻找新的空间(无论是在所需的门还是在所使用的操作码空间方面)。

这是ABI开始渗入ISA的情况之一。你会在RISC-V中发现一些这样的东西。由于我们在标准化ISA时已经移植了一个相当重要的软件堆栈,我们必须对ISA进行微调,以匹配真实代码。由于基本RISC-V ISAs的明确目标是为未来的扩展保留大量的编码空间。

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

作为一个思考练习,让我们假设当存储在RV64I上的X寄存器中时,RISC-V ABI没有强制要求int的高位的规范表示。这里的结果是,现有的W指令族不会特别有用:你可以使用addiw t0, t0, 0作为符号扩展,这样编译器就可以依赖高阶位中的内容,但这会为许多常见模式(如compare+branch)添加一条额外的指令。在这里,正确的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

当我们平衡后,零扩展的软件成本似乎高于符号扩展的硬件成本:对于尽可能小的设计(即微代码实现),你仍然需要算术右移,这样你就不会丢失任何数据路径,对于最大可能的设计(即,一个大的无序内核),代码在分支之前只会四处乱洗比特。奇怪的是,为符号扩展支付可观成本的一个地方是具有短管道的有序机器:您可以减少ALU路径的MUX延迟,这在某些设计中至关重要。在实践中,还有很多其他地方的符号扩展是正确的决定,所以仅仅更改这个不会导致删除该数据路径。

为了扩展公认答案中关于"8位和16位算术很罕见"的评论:一些最常见的计算机语言被设计成不需要它,因为过去流行的ISAs没有

C指定任何比int窄的操作数在进行任何算术运算时都会被"提升"为int。在RISC-V上,int为32位宽。有LB/LBULH/LHU指令在从存储器加载它们时在零扩展unsigned short和符号扩展signed char之间进行选择。

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

最新更新