SEXT and ZEXT in RISC-V
Table of Contents
1. SEXT and ZEXT in RISCV-V
1.1. overview
RISC-V 所有 GPR 都是 XLEN 长度, 导致不同宽度的整型数据进行转换时, 会涉及到 sext/zext 的问题. 以 rv64 为例, 由于不存在 32 位(以及 16/8 位)的 GPR, 导致 32/16/8 位整数必需通过 sext/zext 保存在 64 位 GPR 中.
例如:
- int32 的 0x00000000f0000000 转换为 int64 时需要通过 sext 变成 0xfffffffff0000000
- uint32 的 0x00000000f0000000 转换为 int64 时需要通过 zext 变成 0x00000000f0000000
- int64 的 0x00000000f0000000 转换成 int32 需要通过 sext 变成 0xfffffffff0000000
- uint64 的 0xfffffffff0000000 转换成 uint32 需要通过 zext 变成 0x00000000f0000000
为了消除 sext:
- RISC-V 提供了一些带有 implicit sext 功能的指令
- RISC-V psABI 规定函数参数必须是经过 sext 的, 不需要再对它做 sext
- 通过一些优化, 可以去掉一些不必要的 sext/zext
1.2. sext
1.2.1. implicit sext
有些 RISC-V 指令(大部分都带 w 后缀, 除了 lui, lh, lb 等)只会写目的寄存器的一部分,例如低 32 位, 寄存器中没有被写部分会通过隐式的 sext 补上符号位, 而不是保留原来的数据. 例如:
lw a0, 0(a0) sext.w a0, a0 # <-- 这条指令并不需要, 因为 lw 会隐式的对 a0 做 sext addi a0, a0, 1
另外, `sext.w a0, a1` 实际是 `addiw a0, a1, 0` 的伪指令, 因为 `addiw a0, a1, 0` 隐含着对 a0 的符号扩展
具体可以分为以下几类:
load 数据的宽度小于 XLEN 时, 例如 lh, lb 以及 rv64 的 lw
x[rd] = sext(M[x[rs1] + sext(offset)][31:0])
- lw (rv64), lh, lb 会进行 sext
- lwu (rv64), lhu, lbu 会进行 zext
lui (rv64)
和 load 指令类似, 只不过要 load 的数据来自立即数
x[rd] = sext(immediate[31:12] << 12)
rv64 中的 xxxw 指令, 例如 addiw, slliw, mulw, amoxor.w, …
x[rd] = sext((x[rs1] + sext(immediate))[31:0])
rv{32,64}{f,d} 中的 xxx.x.w, 例如 fmv.x.w
x[rd] = sext(f[rs1][31:0])
1.2.2. sext in gcc
1.2.2.1. implicit sext
gcc 涉及到类型转换时本来需要生成 sext 指令, 但由于 implicit sext, 这些 sext 实际上并没有必要, 例如
int64_t foo(int32_t a, int32_t b) { return a + b; }
# gcc -O0 foo: ... addw a5,a4,a5 sext.w a5,a5 #~~~~~~~~~~~~ #... mv a0,a5 #... jr ra
实际上 addw 包括 implicit sext, 所以 sext.w 并不需要:
# gcc -O2 foo: addw a0,a0,a1 ret
这个优化只需要 md 中的一个 pattern 即可:
(define_insn "*addsi3_extended" [(set (match_operand:DI 0 "register_operand" "=r,r") (sign_extend:DI (plus:SI (match_operand:SI 1 "register_operand" " r,r") (match_operand:SI 2 "arith_operand" " r,I"))))] "TARGET_64BIT" "add%i2w\t%0,%1,%2" # %i2 是指 %2 是 immediate 时输出 `i`, 即 addiw, 否则输出 ``, 即 addw [(set_attr "type" "arith") (set_attr "mode" "SI")])
但目前 gcc (13.2.0) 还不能去掉所有类似的 sext, 例如:
Unnecessary sext.w after amoor.w in __atomic_fetch_or on RV64
可以通过单独的优化 pass 来完成这件事,例如:llvm 的 SExt W Removal, 基本思想是如果 sext.w 操作的寄存器是某个 implicit sext 指令的直接或间接输出,则这个 sext.w 是多余的。
1.2.2.2. explicit sext
指令没有 implicit sext 时, 需要显式的 sext:
int32_t a0(int64_t a) { return a; } int32_t a1(int64_t a, int64_t b) { return a & b; }
a0: sext.w a0,a0 ret a1: and a0,a0,a1 sext.w a0,a0 ret
上面的例子中, sext.w 是必须的, 但编译器可以根据值的范围判定某些显式 sext 是多余的, 例如 gcc 的 combine 可以根据值的范围去掉不需要的 sext.w:
int32_t b0(int64_t a) { return a & 1; } int32_t b1(int64_t a) { return a & 1 << 30; } int32_t b2(int64_t a) { return a & 1 << 31; }
$> riscv-gcc test.c -O2 -c -s b0: andi a0,a0,1 ret b1: li a5,1073741824 and a0,a0,a5 ret b2: li a5,-2147483648 and a0,a0,a5 sext.w a0,a0 ret $> riscv-gcc test.c -O2 -c -s -fdisable-rtl-combine b0: andi a0,a0,1 sext.w a0,a0 ret b1: li a5,1073741824 and a0,a0,a5 sext.w a0,a0 ret b2: li a5,-2147483648 and a0,a0,a5 sext.w a0,a0 ret
1.3. zext
https://forums.sifive.com/t/how-can-i-repeat-the-coremark-score/1947
#include <stdint.h> #ifdef BAD typedef uint32_t INT; #else typedef int32_t INT; #endif void matrix_add_const(INT N, INT *A, INT val) { INT i, j; for (i = 0; i < N; i++) { for (j = 0; j < N; j++) { A[i * N + j] += val; } } }
// good: // riscv64-linux-gnu-gcc-10 test.c -Ofast -c 0000000000000000 <matrix_add_const>: 0: 02a05963 blez a0,32 <.L1> 4: fff5069b addiw a3,a0,-1 8: 1682 slli a3,a3,0x20 a: 82f9 srli a3,a3,0x1e c: 00458793 addi a5,a1,4 10: 00251893 slli a7,a0,0x2 14: 96be add a3,a3,a5 16: 4801 li a6,0 0000000000000018 <.L3>: 18: 87ae mv a5,a1 # a1 是 i*N 000000000000001a <.L4>: 1a: 4398 lw a4,0(a5) 1c: 0791 addi a5,a5,4 1e: 9f31 addw a4,a4,a2 20: fee7ae23 sw a4,-4(a5) 24: fef69be3 bne a3,a5,1a <.L4> 28: 2805 addiw a6,a6,1 2a: 95c6 add a1,a1,a7 2c: 96c6 add a3,a3,a7 2e: ff0515e3 bne a0,a6,18 <.L3> 0000000000000032 <.L1>: 32: 8082 ret // bad: // riscv64-linux-gnu-gcc-10 test.c -Ofast -c -DBAD 0000000000000000 <matrix_add_const>: 0: 882a mv a6,a0 2: 4301 li t1,0 4: 4881 li a7,0 6: c505 beqz a0,2e <.L9> 0000000000000008 <.L2>: 8: 871a mv a4,t1 000000000000000a <.L4>: ,---- 计算偏移量时需要做无符号扩展 | a: 02071793 slli a5,a4,0x20 | e: 83f9 srli a5,a5,0x1e `---- 10: 97ae add a5,a5,a1 12: 4394 lw a3,0(a5) 14: 2705 addiw a4,a4,1 16: 9eb1 addw a3,a3,a2 18: c394 sw a3,0(a5) 1a: fee818e3 bne a6,a4,a <.L4> 1e: 2885 addiw a7,a7,1 20: 0065033b addw t1,a0,t1 24: 0105083b addw a6,a0,a6 28: ff1510e3 bne a0,a7,8 <.L2> 2c: 8082 ret
rv64 下 uint32_t 需要显式的无符号扩展的场景有:
uint32 -> 64
例如:
#include <stdint.h> int64_t foo(uint32_t N) { return N; }
0000000000000000 <foo>: 0: 1502 slli a0,a0,0x20 2: 9101 srli a0,a0,0x20 4: 8082 ret
- 像上面的例子, 做为地址使用. 实际上也是 uint32_t -> 64, 因为 riscv 内存寻址时必须使用整个基地址寄存器的所有 bit (例如它没有办法只使用 a0 的低 32 位)
- …
上面的问题最根本的原因是, int32_t/uint32_t 的在做为地址时需要扩展成 64bit, 而 riscv 大部分指令会自动做符号扩展, 所以针对 int32_t 的符号扩展可以省掉, 但无符号扩展则必须通过显式的 zext 类指令(例如 slli+srli), 所以 rv64 上涉及访存的变量用 int32_t 性能好于 uint32_t. 实际上我们应当使用 size_t 来索引数组(rv64 下 size_t 为 uint64_t, rv32 下 size_t 为 uint32_t)
1.4. 数据转型
许多情况下 sext/zext 都是因为参数和返回值的数据转型导致的, riscv psABI 中关于参数和返回值的 sext 规定:
Scalars that are at most XLEN bits wide are passed in a single argument register, or on the stack by value if none is available. When passed in registers or on the stack, integer scalars narrower than XLEN bits are widened according to the sign of their type up to 32 bits, then sign-extended to XLEN bits.
数据转型包括以下的情形:
,---- 64 -> 32 时需要 sext.w, 因为 psABI 规定比 XLEN 窄的返回值一定经过 sext, 即使是 uint32 | | int32_t a0(int64_t a) { return a; } | uint32_t a1(int64_t a) { return a; } | int32_t a2(uint64_t a) { return a; } | uint32_t a3(uint64_t a) { return a; } | | a0: | sext.w a0,a0 | ret | a1: | sext.w a0,a0 | ret | a2: | sext.w a0,a0 | ret | a3: | sext.w a0,a0 | ret `---- ,---- int32 -> 64 不需要 sext.w, 因为 psABI 规定比 XLEN 窄的参数一定经过 sext | | int64_t b0(int32_t a) { return a; } | uint64_t b1(int32_t a) { return a; } | | b0: | ret | b1: | ret `---- ,---- uint32 -> 64 时需要 zext.w, 因为 psABI 没有规定 uint32 需要经过 zext | | int64_t b2(uint32_t a) { return a; } | uint64_t b3(uint32_t a) { return a; } | | b2: | slli a0,a0,32 | srli a0,a0,32 | ret | b3: | slli a0,a0,32 | srli a0,a0,32 | ret `----
1.5. 其它
由于符号扩展会在高位加入符号位, 所以涉及 bit 操作时用 signed 则可能出错, 例如:
// 2023-04-27 20:48 #include <stdint.h> #include <stdio.h> int main(int argc, char *argv[]) { int8_t tmp = -1; int8_t sign = tmp & (1 << 7); /* NOTE: gcc 先把左侧 sign 符号扩展成 int32_t, 为 0xffffff80, 右侧也是以 * int32_t 为计算, 导致结果为 0x00000080. 如果 int8_t 换成 uint8_t 则没有问 * 题, 因为没有符号扩展. 所以 bit 操作最好用 unsigned */ if (sign == (tmp & (1 << 7))) { printf("ok\n"); } return 0; }
综上, 由于有符号/无符号扩展的原因, riscv 用 signed int 可能性能好一些, 但 unsigned int 更不容易出一些奇怪的错误.