
我们已经了解\(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左移sllild读内存
伪指令的定义可参考《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/sltusra/srlint 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, bgeubgt, 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可以摆烂
ud2ebreakteq假设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代码, 即可预测其将如何在计算机上运行