mtune

Table of Contents

1. mtune

mtune 包含两部分内容:

  1. microarchitecture
  2. 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

Author: [email protected]
Date: 2022-05-16 Mon 17:00
Last updated: 2024-03-24 Sun 12:16

知识共享许可协议