mtune
Table of Contents
1. mtune
mtune 包含两部分内容:
- microarchitecture
- riscv_tune_param
static const struct riscv_tune_info riscv_tune_info_table[] = { /* name, microarchitecture, riscv_tune_param */ { "rocket", generic, &rocket_tune_info }, { "sifive-3-series", generic, &rocket_tune_info }, { "sifive-5-series", generic, &rocket_tune_info }, { "sifive-7-series", sifive_7, &sifive_7_tune_info }, { "size", generic, &optimize_size_tune_info }, };
1.1. riscv_tune_param
以 `-mtune=sifive-7-series` 为例, 其对应的 tune_param 为
static const struct riscv_tune_param rocket_tune_info = { /* fp_add(SFMode) 的 cost 相当于 4 个 nop */ /* fp_add(DFMode) 的 cost 相当于 5 个 nop */ {COSTS_N_INSNS(4), COSTS_N_INSNS(5)}, /* fp_add */ {COSTS_N_INSNS(4), COSTS_N_INSNS(5)}, /* fp_mul */ {COSTS_N_INSNS(20), COSTS_N_INSNS(20)}, /* fp_div */ /* int_mul(SIMode) 相当于 4 个 nop */ /* int_mul(DIMode) 相当于 4 个 nop */ {COSTS_N_INSNS(4), COSTS_N_INSNS(4)}, /* int_mul */ {COSTS_N_INSNS(6), COSTS_N_INSNS(6)}, /* int_div */ 1, /* issue_rate */ 3, /* branch_cost */ 5, /* memory_cost */ true, /* slow_unaligned_access */ };
mtune 参数有一个特殊的选项叫 `size`, 它对应的 tune_param 为
static const struct riscv_tune_param optimize_size_tune_info = { {COSTS_N_INSNS (1), COSTS_N_INSNS (1)}, /* fp_add */ {COSTS_N_INSNS (1), COSTS_N_INSNS (1)}, /* fp_mul */ {COSTS_N_INSNS (1), COSTS_N_INSNS (1)}, /* fp_div */ {COSTS_N_INSNS (1), COSTS_N_INSNS (1)}, /* int_mul */ {COSTS_N_INSNS (1), COSTS_N_INSNS (1)}, /* int_div */ 1, /* issue_rate */ 1, /* branch_cost */ 2, /* memory_cost */ false, /* slow_unaligned_access */ };
可见大部分的 cost 都很小, 这会导致许多基于 cost 的优化不会工作, 因为这些优化通常都会生成更多的 insn
另外, `-Os` 会强制 mtune 为 size:
cpu = riscv_parse_tune( riscv_tune_string ? riscv_tune_string : (riscv_cpu_string ? riscv_cpu_string : RISCV_TUNE_STRING_DEFAULT)); /*NOTE: -Os 时 optimize_size 为 true */ tune_param = optimize_size ? &optimize_size_tune_info : cpu->tune_param;
1.1.1. add, mul, div
表示执行 {fp,int}add,mul,div 等操作的 cost, 例如 GCC Expand Mult 会使用这个 cost 决定 mul 是否转换成 shift
1.1.2. slow_unaligned_access
TARGET_SLOW_UNALIGNED_ACCESS 会使用这个 tune_param
表示 arch 的 unaligned access 是否是 slow, 例如:
struct __attribute__((packed)) X { short a; int b; }; - void foo(int x) { struct X z; z.a = 1; z.b = 2; }
默认情况下 slow_unaligned_access 为 true, 导致针对 z.b 的访问不会通过 sw 指令, 而要通过两个 sh
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.c -O0 -c $> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -d ./test.o 0000000000000000 <foo>: 0: 7179 addi sp,sp,-48 2: f422 sd s0,40(sp) 4: 1800 addi s0,sp,48 6: 87aa mv a5,a0 8: fcf42e23 sw a5,-36(s0) c: 4785 li a5,1 e: fef41423 sh a5,-24(s0) 12: fea45783 lhu a5,-22(s0) 16: 8b81 andi a5,a5,0 18: 0027e793 ori a5,a5,2 1c: fef41523 sh a5,-22(s0) 20: fec45783 lhu a5,-20(s0) 24: 8b81 andi a5,a5,0 26: fef41623 sh a5,-20(s0) 2a: 0001 nop 2c: 7422 ld s0,40(sp) 2e: 6145 addi sp,sp,48 30: 8082 ret
riscv 上指定 `-mtune=size` 使 slow_unaligned_access 为 false, 则 gcc 会使用 sw 访问没有对齐的 z.b
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.c -O0 -c -mtune=size $> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -d ./test.o 0000000000000000 <foo>: 0: 7179 addi sp,sp,-48 2: f422 sd s0,40(sp) 4: 1800 addi s0,sp,48 6: 87aa mv a5,a0 8: fcf42e23 sw a5,-36(s0) c: 4785 li a5,1 e: fef41423 sh a5,-24(s0) 12: 4789 li a5,2 14: fef42523 sw a5,-22(s0) 18: 0001 nop 1a: 7422 ld s0,40(sp) 1c: 6145 addi sp,sp,48 1e: 8082 ret
另外, 通过 `-mstrict-align` 相当于 slow_unaligned_access 为 true, 即认为系统不会自动处理 unaligned access
上面例子的实现在 expand_assignment, 它会考虑 slow_unaligned_access 决定 store_bit_field 时生成 `and, or…` 来操作 bitfield (具体代码在 store_fixed_bit_field_1) 还是直接 `sw`
另外, 编译器会尽量推导出访问是否对齐, 但不可能在运行时完全避免非对齐的访问, 例如:
$> cat test.c #include <stdint.h> void foo(int64_t *x) { *x = 1; } int main(int argc, char *argv[]) { int32_t x[4] = {0}; foo((int64_t *)x); foo((int64_t *)(x + 1)); } $> riscv64-unknown-linux-gnu-gcc test.c -c -O3 $> riscv64-unknown-linux-gnu-objdump -d test.o Disassembly of section .text: 0000000000000000 <foo>: 0: 4785 li a5,1 2: e11c sd a5,0(a0) 4: 8082 ret
两次 foo 调用至少有一次会导致 unaligned access, 这里就需要 trap_handler 来处理了 (参考 Spike 中的 handle_misaligned_fetch)
或者通过 may_alias 的指明存在 type punning:
$> cat test.c #include <stdint.h> typedef int64_t __attribute__((may_alias, aligned(1))) misaligned_int64_t; void foo(misaligned_int64_t *x) { *x = 1; } $> riscv64-unknown-linux-gnu-gcc test.c -c -O3 $> riscv64-unknown-linux-gnu-objdump -d test.o 0000000000000000 <foo>: 0: 4785 li a5,1 2: 00f51023 sh a5,0(a0) 6: 00051123 sh zero,2(a0) a: 00051223 sh zero,4(a0) e: 00051323 sh zero,6(a0) 12: 8082 ret
由于 unalignment acess 对性能有影响, 所以可以通过 sanitize 工具检查一下:
$> cat test.c #include <stdint.h> void foo(int64_t *x) { *x = 1; } int main(int argc, char *argv[]) { int32_t x[4] = {0}; foo((int64_t *)x); foo((int64_t *)(x + 1)); } $> gcc test.c -O3 -fsanitize=alignment $> ./a.out test.c:3:27: runtime error: store to misaligned address 0x7fff61466244 for type 'int64_t', which requires 8 byte alignment 0x7fff61466244: note: pointer points here 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 01 00 00 00 00 00 00 82 71 61 95 c3 2e 2e
Backlinks
GCC Target Hook (GCC Target Hook > cost 相关 > TARGET_SLOW_UNALIGNED_ACCESS): TARGET_SLOW_UNALIGNED_ACCESS
Unaligned Memory Access (Unaligned Memory Access): 如果编译器能判断出当前指令是 UMA, 则会根据 slow_unaligned_access 和 `-mstrict-align` 参数决定对于 UMA 是直接生成 load/store 指令还是通过其它多条指令. 例如, 为了支持非对齐的 sw, 编译器可能会使用 4 条 sb 指令
1.1.3. issue_rate
TARGET_SCHED_ISSUE_RATE 会使用这个 tune_param
1.1.4. branch_cost
以 `abs(x)` 为例, expand_abs 时会根据 branch_cost 决定生成 `if (x<0) {-x} else {x}` 还是通过其它位运算的形式得到 abs(x)
int foo(int k) { return abs(k); }
当增大 branch_cost 时会生成这样的代码:
0000000000000000 <foo>: 0: 41f5579b sraiw a5,a0,0x1f 4: 8d3d xor a0,a0,a5 6: 9d1d subw a0,a0,a5 8: 8082 ret
当减小 branch_cost 时会生成使用 branch 的代码:
0000000000000000 <foo>: 0: 87aa mv a5,a0 2: 00055463 bgez a0,a <.L2> 6: 40a007bb negw a5,a0 000000000000000a <.L2>: a: 0007851b sext.w a0,a5 e: 8082 ret
另外, 上面这个例子也展示了 gcc builtin 的好处:
gcc builtin 里包含了一些常用的数学函数例如 abs, 当使用 builtin 的 abs 时, gcc 的 expand_abs 可以根据 branch_cost 决定生成什么样的代码, 但 libc 的 abs 则没有这种灵活性
虽然使用 mtune=size 把 branch_cost 设为 1, 但 libc 的 abs 的实现是固定的, 不会改变.
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.c -O2 -fno-builtin -nostartfiles -static -mtune=size $> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -d ./a.out 00000000000100b0 <foo>: 100b0: a009 j 100b2 <abs> 00000000000100b2 <abs>: 100b2: 41f5579b sraiw a5,a0,0x1f 100b6: 8d3d xor a0,a0,a5 100b8: 9d1d subw a0,a0,a5 100ba: 8082 ret
另外, pass_data_tree_ifcombine 也会考虑 branch_cost 的影响: 当 branch_cost 较大时, 会把 `if (a) {if (b) {}}` 优化成 `if (a&b)`
Backlinks
GCC Builtin (GCC Builtin > builtin optimization): 另外, builtin 还可以利用 mtune 信息产生针对某一个 tune 的优化代码, 但 libc 中只 能产生针对某个 arch 的 `通用` 代码. 例如 branch_cost
1.1.5. memory_cost
riscv_rtx_costs: /*...*/ case MEM: /* addressing cost, 例如 lui/auipc/got/ */ if ((cost = riscv_address_insns(XEXP(x, 0), mode, true)) > 0) { /* memory cost */ *total = COSTS_N_INSNS(cost + tune_param->memory_cost); return true; } /*...*/
1.2. microarchitecture
microarchitecture 主要是在 md 中定义 insn-latency 供 scheduler 使用
1.3. llvm mtune
// gcc md: (define_insn_reservation "sifive_7_fdiv_s" 27 (and (eq_attr "tune" "sifive_7") (eq_attr "type" "fdiv,fsqrt") (eq_attr "mode" "SF")) "sifive_7_B,sifive_7_fpu*26") // llvm td: defm FSQRT_S : FPUnaryOp_r_frm_m<0b0101100, 0b00000, FFINX, "fsqrt.s">, Sched<[WriteFSqrt32, ReadFSqrt32]>; def : WriteRes<WriteFSqrt32, [SiFive7PipeB, SiFive7FDiv]> { let Latency = 27; let ResourceCycles = [1, 26]; }
def RocketModel : SchedMachineModel { let MicroOpBufferSize = 0; // Rocket is in-order. let IssueWidth = 1; // 1 micro-op is dispatched per cycle. let LoadLatency = 3; let MispredictPenalty = 3; let CompleteModel = false; }
Backlinks
GCC Backend (GCC Backend > misc > mcpu/mtune/march/mabi > mtune): mtune
GCC Cost (GCC Cost > mtune): mtune 中定义了 branch_cost, memory_cost, fp_add/fp_mul 等 cost