我们已经了解\(S_C\rightarrow S_{isa}\)
本次课内容: 以RISC-V为例, 进一步解释映射的结果
int f() { return 0x123; /* 291 */ }
int g() { return -1; }
int h() { return 0x1234; /* 4660 */ }
int i() { return 0xbb8; /* 3000 */ }
理解lui
中的imm[11]
:
一大步(lui
) + 一小步(addi
)
addi
的语义, imm[11:0]
需要符号扩展
lui
结果进行±2048范围内的调整(可仔细推敲边界情况)lui
需要先装入一个距离imm
不超过±2048范围的数
imm[11:0]
为负, 则lui
需先右移4096,
再让addi
左移slli
ld
读内存
伪指令的定义可参考《RISC-V汇编语言编程手册》
用两个32位寄存器联合存放一个64位数据
RISC-V的两套整数ABI
ILP32 ABI (RV32)
大小 | 对齐 | |
---|---|---|
bool/_Bool | 1 | 1 |
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 4 |
long long | 8 | 8 |
void * | 4 | 4 |
float | 4 | 4 |
double | 8 | 8 |
LP64 ABI (RV64)
大小 | 对齐 | |
---|---|---|
bool/_Bool | 1 | 1 |
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 8 | 8 |
long long | 8 | 8 |
void * | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
\(\{v_1, v_2, \dots\}\rightarrow \{r_1, r_2, \dots, M\}\)
在实际的电路中
C程序中的变量远多于寄存器, 该如何分配?
+----------+
| |
+----------+
| stack |
+----------+
| | |
| v |
| |
| ^ |
| | |
+----------+
| heap |
+----------+
| data |
+----------+
| text |
+----------+
| |
0 +----------+
#define def(type, name) \
volatile type name ## _a; \
volatile type name ## _b; \
void f_##name () { name ## _a = name ## _b; }
def(_Bool, _Bool)
def(char, char)
def(signed char, signed_char)
def(short, short)
def(unsigned short, unsigned_short)
def(int, int)
def(unsigned int, unsigned_int)
def(long, long)
def(long long, long_long)
def(void *, void_)
def(float, float)
def(double, double)
一个例外:
unsigned int
在RV64中使用lw
而不是lwu
ILP32 ABI (RV32)
大小 | 指令 | |
---|---|---|
bool/_Bool | 1 | lbu /sb |
char | 1 | lbu /sb |
signed char | 1 | lb /sb |
short | 2 | lh /sh |
unsigned short | 2 | lhu /sh |
int | 4 | lw /sw |
unsigned int | 4 | lw /sw |
long | 4 | lw /sw |
long long | 8 | lw+lw /sw+sw |
void * | 4 | lw /sw |
float | 4 | flw /fsw |
double | 8 | fld /fsd |
LP64 ABI (RV64)
大小 | 指令 | |
---|---|---|
bool/_Bool | 1 | lbu /sb |
char | 1 | lbu /sb |
signed char | 1 | lb /sb |
short | 2 | lh /sh |
unsigned short | 2 | lhu /sh |
int | 4 | lw /sw |
unsigned int | 4 | lw /sw |
long | 8 | ld /sd |
long long | 8 | ld /sd |
void * | 8 | ld /sd |
float | 4 | flw /fsw |
double | 8 | fld /fsd |
不对齐即addr(n) % align(n) != 0
,
如int
变量分配在地址0x13
在x86上实测不对齐访存的性能
#include <stdlib.h>
#define LOOP 2000000
#define SIZE 10000
char buf[SIZE + 64] __attribute((aligned(64))); // 64 = 缓存块的大小(字节)
int main(int argc, char *argv[]) {
int offset = atoi(argv[1]);
for (int n = LOOP; n != 0; n --) {
for (char *p = buf + offset; p < buf + SIZE; p += 64) { *(long *)p = 1; }
}
return 0;
}
在x86上, 当不对齐的数据跨越64字节边界时, 可观察到约2x的性能下降
C运算符 | RISC-V指令 |
---|---|
+ , - ,
* , / , % |
add , sub ,
mul , div , rem |
= |
mv , 访存指令 |
& , | ,
^ |
and , or ,
xor |
~ |
xori r, r, -1 |
<< ,
>> |
sll , srl ,
sra |
! |
sltiu r, r, 1 |
< ,
> |
slt |
long f1(long a, long b) { return a + b; }
long f2(long a, long b) { return a - b; }
long f3(long a, long b) { return a * b; }
long f4(long a, long b) { return a / b; }
// ...
通过-O1
编译成汇编文件, 即可了解二者的联系
-O0
- 每次计算前先从内存读出变量,
每次计算后马上写回内存-O1
- 开始计算前从内存读出变量, 计算过程在寄存器中进行,
计算全部结束后再写回内存-O2
- 编译器直接把结果算好了 😂
#include <stdint.h>
int32_t add1( int32_t a, int32_t b) { return a + b; }
uint32_t add2(uint32_t a, uint32_t b) { return a + b; }
int32_t cmp1( int32_t a, int32_t b) { return a < b; }
int32_t cmp2(uint32_t a, uint32_t b) { return a < b; }
int32_t shr1( int32_t a, int32_t b) { return a >> b; }
uint32_t shr2(uint32_t a, int32_t b) { return a >> b; }
int64_t zext1( int32_t a) { return a; }
uint64_t zext2(uint32_t a) { return a; }
add1
和add2
的代码完全一样
slt
/sltu
sra
/srl
int f(int x) {
int y = 0;
if (x > 500) y = 150;
else if (x > 300) y = 100;
else if (x > 100) y = 75;
return y;
}
用条件跳转指令决定是否进入代码块
beq
, bne
, blt
,
bge
, bltu
, bgeu
bgt
, ble
,
bgtu
, bleu
, 通过交换指令的操作数实现
bgt r1, r2, offset
等价于blt r2, r1, offset
上述指令可覆盖有/无符号数的所有比较运算
int f(int x) {
int y = 0;
switch (x) {
case 1: y = 2; break;
case 2: case 3: y = 5; break;
case 4: case 5: case 6: case 7: y = 8; break;
case 8: case 9: case 10: case 11: y = 10; break;
case 12: y = 15; break;
default: y = -1; break;
}
return y;
}
-O0
或-O1
编译,
gcc生成了跳转表(x
->分支入口的偏移)
lw
读出跳转表中的偏移, 计算出分支入口,
再通过jr
跳转到分支-O2
编译本例,
gcc直接生成了结果查找表(x
->y
)
x
对应的y
, 连跳转都省了case 1
, case 10
,
case 100
, gcc则生成if-else
风格的代码
循环的机器级表示 = 一条往回跳的条件跳转指令
#include <stdio.h>
#include <limits.h>
int foo(int x) { return (x + 1) > x; }
int main() {
printf("INT_MAX=%d, cmp: %d%d\n", INT_MAX, (INT_MAX + 1) > INT_MAX, foo(INT_MAX));
return 0;
}
表面上语义等价的代码, 运行结果却不同
原因: 有符号整数加法溢出是UB
一套ISA只会使用指定的一种编码方式表示有符号数(大部分是补码)
最大正数 + 1 = 最小负数
addu/addiu
- 回滚, add/addi
- 抛异常
u
充满误导性,
大部分组原老师认为有/无u
分别对应C语言中的有/无符号加法
事实: 大多数编程语言不要求有符号加法溢出时抛异常
add/addi
显得很鸡肋
addi
#include <stdio.h>
int main() {
int i = 30;
printf("1 << %d = 0x%08x\n", i, 1 << i); i ++;
printf("1 << %d = 0x%08x\n", i, 1 << i); i ++;
printf("1 << %d = 0x%08x\n", i, 1 << i); i ++;
printf("1 << %d = 0x%08x\n", i, 1 << i); i ++;
return 0;
}
是否采用-O2
编译, 得到不同结果
#include <stdio.h>
int f(int a, int b) { return a / b; }
__attribute__((noinline)) int g() { return 1 / 0; }
__attribute__((noinline)) int h(int a, int b) { return a / b; }
int main() {
printf("1 / 0 = %d\n", 1 / 0);
printf("f(1, 0) = %d\n", f(1, 0));
printf("g(1, 0) = %d\n", g());
printf("h(1, 0) = %d\n", h(1, 0));
return 0;
}
C语言规定, 整数除零是UB, 于是编译器可以任意处理
-1
但因为从语言层面来说就是UB, 添加-O2
之后gcc可以摆烂
ud2
ebreak
teq
假设int
通过补码表示, 长度32位
表达式 | 结果 | | | 表达式 | 结果 |
---|---|---|---|---|
UINT_MAX+1 |
0 |
| | 1<<-1 |
undefined |
LONG_MAX+1 |
undefined | | | 1<<0 |
1 |
INT_MAX+1 |
undefined | | | 1<<31 |
undefined |
SHRT_MAX+1 |
INT_MAX==SHRT_MAX 则undefined |
|| | 1<<32 |
undefined |
char c=CHAR_MAX; c++ |
??? | | | 1/0 |
undefined |
-INT_MIN |
undefined | | | INT_MIN/-1 |
undefined |
意义: 大家写C代码, 即可预测其将如何在计算机上运行