被开放 ISA 冲昏了头脑,只做了 RISC-V:虎胆龙威(1,2)
注意到 ALU 基本完全不能用了,于是就想到用等价指令序列代替样例的 riscv_straight.S 中坏掉的指令,而代替 ALU 功能最简单的方法就是查表啦。但是这道题只有 4KiB 存储器空间,所以我们只能为 1-byte 的数据打表,对一个完整的 4-byte 字长分四次处理:
- 减法:为非运算打表,这样可以把减法转化成负数相加,代码中实现为
sub_num
宏,使用 gen-not-list.lua 生成表。 - 相等性比较与跳转:打一个除 index = 0 为 4(rv32i 单条指令长度),其他位置都为 0 的表,为减法结果的每个字节查表并累加,结果加一段特殊的指令序列的地址进行相对跳转。这段指令序列只需要稍微布局一下即可按照减法结果是否为 0 跳转到不同目标。代码中实现为
jmp_not_equal
宏,使用 gen-sum-list.lua 生成表。 - 大小比较:我们只需要实现 bltu(branch if less than, unsigned)的功能即可。将两数相减后只需检查最高一字节即可,我们用类似相等性比较与跳转的方式打表将符号位是正是负转化为跳转的偏移量即可。代码中实现为
jump_if_less
宏,使用 gen-cmp-list.lua 生成表。
完整代码见 1.S。这份代码写的时候做了多余的假设(一开始没意识到 auipc
是可以用的,所以特殊布局了内存;没意识到 lui
是可以用的,所以用 addi 或者读内存替换了几处 li
的立即数加载),但懒得改了()。
每个内存地址只能读一次,这同时影响数据和代码:
- 代码上,我们不能前向跳转,无法写出循环
- 数据上,我们必须一次性读完全部的待排序数据
前者的解决方案相当明了,只需要就地循环展开即可(我们一共有 16 个待排元素,利用选择排序 16 * 16 / 2 = 128 轮操作,如果每个比较操作在 4 条指令左右,使用 2KiB 存储尚可以接受)。对于后者,RISC-V 32 位整数指令集是一个有 32 个寄存器槽位的大 ISA,我们可以直接把全部待排序数据加载进寄存器(代码中选用 x16-x31),排序完再一次性写回去。
至于循环展开要怎么写,有几种办法:
- 给编译器传 unroll loop 的 hint,但考虑到在如此受限的环境跑 C 代码本身就相当灵车还要准备 crt,而且编译器行为没那么可控,此方法被放弃。
- 用汇编器的宏(
.macro
)做 codegen,然而 clang 默认的递归深度只有 20 层被爆栈了,此方法被放弃 - 最后选择了用图灵完备的 m4 宏语言预处理一遍源代码。
完整代码见 2.m4,实现了选择排序,
regidx
和numreg
分别根据待排数据的编号生成对应寄存器的编号和全名(16-31,x16-x31)gen_load
与gen_store
分别生成加载待排数据和写入已排序数据的指令序列gen_cmp_and_set
进行一次比较并选择出较小的数据cmp_and_select
为剩余的 N 个元素生成 N 次比较序列