Тулчейн RISC-V и компиляторные оптимизации

Лабораторная работа

Разбить на страницы
Показывать лекцию целиком

Данная лабораторная работа подготовлена Кириленко Яковом Александровичем по заказу Альянса RISC-V, допускается к использованию под лицензией CC BY 4.0.

Приведённые ниже команды рассчитаны на выполнение в рабочей среде Syntacore Kit, распространяемой Альянсом RISC-V в образовательных целях. Обратите внимание, что в другом окружении приведённые примеры команд могут не работать или работать иначе.

Рассмотрите программу, считающую скалярное произведение векторов.

$$main.c$$

#include <stdlib.h>
#include <stdio.h>
#include <memory.h>
#include <time.h>
#include <malloc.h>

const size_t N = 200000;
const size_t K = 2000;

typedef  float cell_t;
typedef cell_t * cell_ptr_t;

cell_t dot_product(cell_ptr_t a, cell_ptr_t b, size_t n) {
    cell_t r = 0;
    for (size_t i = 0; i < n; ++i) {
        r += a[i] * b[i];
    }
    return r;
}

cell_ptr_t allocate() {
    cell_ptr_t r = (cell_t*) malloc(sizeof(cell_t) * N);
    for (size_t i = 1; i < N; ++i) {
        r[i] = (cell_t) (N *  rand() / RAND_MAX);
    }
    return r;
}

int main() {
    srand(0); // reproducible PRNG
    cell_ptr_t a = allocate();
    cell_ptr_t b = allocate();
    cell_t r1 = 0;
    clock_t t = clock();
    for (size_t i = 0; i < K; ++i) {
        int x = (t % (i + 1)); // some number to force dot_product call
        r1 += dot_product( (x  2) ? a : b
                        , (x  1) ? b : a
                        , N);
    }
    double time1 = (double)(clock()-t)/CLOCKS_PER_SEC;

    printf("%.3g\n%.3g\n"
        , r1
        , time1);

    free((void *)a);
    free((void *)b);
    return 0;
}

Кросс-компиляция и запуск

Перед выполнением команд проверьте, определены ли переменные GCC_ROOT или RISCV и указан ли в переменной PATH путь /opt/syntacore/sc-dt/2023.08/tools/ (2023.08-версия Syntacore Kit):

env | grep 'GCC_ROOT\|RISCV'
echo $PATH

Если нет, добавьте в файл ~/.bashrc следующие строки (пути зависят от версии используемой рабочей среды):

Если переменая $$RISCV$$ не определена

export GCC_ROOT="/opt/syntacore/sc-dt/2023.08/riscv-gcc"
export PATH=$PATH:/opt/syntacore/sc-dt/2023.08/tools/bin

Если переменая $$RISCV$$ определена

export GCC_ROOT=$RISCV
export PATH=$PATH:/opt/syntacore/sc-dt/2023.08/tools/bin

Скомпилируйте программу под RISC-V. В Syntacore Kit для систем компиляции GCC и CLang это делается следующими способами:

GCC CLang
riscv64-unknown-linux-gnu-gcc main.c -o main
clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -o main main.c

Убедитесь, что полученный исполняемый файл предназначен для RISC?V:

file ./main
./main: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, for GNU/Linux 4.15.0, not stripped

Запустите скомпилированную программу в эмуляторе qemu:

qemu-riscv64 -L "$GCC_ROOT/sysroot" ./main

Отладка

Скомпилируйте программу под RISC-V c отладочной информацией, выставив флаг -g:

GCC CLang
riscv64-unknown-linux-gnu-gcc -g main.c -o main
clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -g -o main main.c

Запустите qemu с сервером GDB на произвольном порту, например 12345:

qemu-riscv64 -L $GCC_ROOT/sysroot -g 12345 ./main

Он будет ждать подключения GDB. Затем запустите GDB из системы компиляции для RISC-V (с использованием отладочных символов из файла ./main):

riscv64-unknown-linux-gnu-gdb ./main

В консоли GDB подключитесь к GDB-серверу, запущенном в qemu:

target remote localhost:12345

Установите sysroot, чтобы GDB мог подгрузить символы используемых динамических библиотек:

set sysroot /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot

Затем выставьте точку останова:

b main

Продолжите выполнение программы:

с

Теперь можно отлаживать программу, запущенную в эмуляторе.

Листинг консоли GDB после проделанных операций

GNU gdb (GDB) 13.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./main...
(No debugging symbols found in ./main)
(gdb) target remote localhost:12345
Remote debugging using localhost:12345
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0x00007f5ae4fdfb40 in ?? ()
(gdb) set sysroot /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot
Reading symbols from /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot/lib/ld-linux-riscv64-lp64d.so.1...
(No debugging symbols found in /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot/lib/ld-linux-riscv64-lp64d.so.1)
(gdb) b main
Breakpoint 1 at 0x1077e
(gdb) c
Continuing.

Breakpoint 1, 0x000000000001077e in main ()
(gdb)

Оптимизация

Рассмотрите листинги ассемблерного кода, полученные Clang при разных уровнях оптимизации, выполнив следующие команды:

  • без оптимизаций
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main.s main.c
    
  • с флагом -O1
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main-O1.s main.c -O1
    
  • с флагом -O2
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main-O2.s main.c -O2
    
  • Будет получен следующий ассемблерный код для функции dot_product:

    Компиляция без оптимизаций

    dot_product:                            # @dot_product
    # %bb.0:
        addi    sp, sp, -64
        sd  ra, 56(sp)                      # 8-byte Folded Spill
        sd  s0, 48(sp)                      # 8-byte Folded Spill
        addi    s0, sp, 64
        sd  a0, -24(s0)
        sd  a1, -32(s0)
        sd  a2, -40(s0)
        li  a0, 0
        sw  a0, -44(s0)
        sd  a0, -56(s0)
        j   .LBB0_1
    .LBB0_1:                                # =>This Inner Loop Header: Depth=1
        ld  a0, -56(s0)
        ld  a1, -40(s0)
        bgeu    a0, a1, .LBB0_4
        j   .LBB0_2
    .LBB0_2:                                #   in Loop: Header=BB0_1 Depth=1
        ld  a0, -24(s0)
        ld  a1, -56(s0)
        slli    a1, a1, 2
        add a0, a0, a1
        flw ft0, 0(a0)
        ld  a0, -32(s0)
        add a0, a0, a1
        flw ft1, 0(a0)
        flw ft2, -44(s0)
        fmadd.s ft0, ft0, ft1, ft2
        fsw ft0, -44(s0)
        j   .LBB0_3
    .LBB0_3:                                #   in Loop: Header=BB0_1 Depth=1
        ld  a0, -56(s0)
        addi    a0, a0, 1
        sd  a0, -56(s0)
        j   .LBB0_1
    .LBB0_4:
        flw fa0, -44(s0)
        ld  ra, 56(sp)                      # 8-byte Folded Reload
        ld  s0, 48(sp)                      # 8-byte Folded Reload
        addi    sp, sp, 64
        ret
    

    В строках 3-12 происходит формирование кадра стека функции dot_product: выделяется необходимое для аргументов и локальных переменных место на стеке (строка 3), на стеке сохраняется адрес возврата и адрес предыдущего кадра (строки 4-5), в регистр сохраняется адрес текущего кадра (строка 6), переданные аргументы a, b и n загружаются на стек (строки 7-9), локальные переменные r и i инициализируются нулями (строки 10-12).

    В строках 15-17 вычисляется, нужно ли выполнять очередную итерацию цикла: со стека в регистры загружаются значения переменных i и n (строки 15-16), а затем сравниваются (строка 17).

    В строках 20-30 происходит вычисление очередной итерации цикла: со стека в регистр загружается значение переменной i (строка 21), вычисляются адреса в памяти значений a[i] и b[i] и они загружаются в регистры (строки 20, 22-27), со стека в регистр загружается значение переменной r (строка 28), к значению r прибавляется результат a[i] * b[i] (строка 29), новое значение r записывается на стек (строка 30).

    В строках 33-35 происходит увеличение счётчика цикла i после выполнения очередной итерации: со стека в регистр загружается значение переменной i (строка 33), значение переменной i увеличивается на 1 (строка 34), новое значение переменной i записывается на стек (строка 35).

    В строках 38-42 происходит возврат результата после выполнения цикла: со стека в регистр, через который возвращается результат, загружается значение переменной r (строка 38), со стека в регистры загружаются адрес возврата и адрес предыдущего кадра стека (строки 39-40), очищается кадр стека (строка 41), происходит возврат из функции dot_product (строка 42).

    Компиляция с флагом $$-O1$$

    dot_product:                            # @dot_product
    # %bb.0:
        fmv.w.x fa0, zero
        beqz    a2, .LBB0_2
    .LBB0_1:                                # =>This Inner Loop Header: Depth=1
        flw ft0, 0(a0)
        flw ft1, 0(a1)
        fmadd.s fa0, ft0, ft1, fa0
        addi    a2, a2, -1
        addi    a1, a1, 4
        addi    a0, a0, 4
        bnez    a2, .LBB0_1
    .LBB0_2:
        ret
    

    Полученный ассемблерный код значительно короче, полученного без применения оптимизаций. Это получается за счёт того, что на стеке не выделяется место под аргументы и локальные переменные, и все вычисления производятся с регистрами без обращений к памяти.

    В строке 3 происходит инициализация регистра, в котором хранится значение r, нулём.

    В строке 4 происходит сравнение регистра, в котором хранится значение n, с нулём, чтобы начать выполнение цикла.

    В строках 6-7 происходит загрузка в регистры значений a[0] и b[0].

    В строке 8 к значению r прибавляется результат a[0] * b[0].

    В строке 9 значение n уменьшается на 1.

    В строках 10-11 увеличиваются значения регистров, в которых хранятся адреса массивов a и b, чтобы на следующей итерации a[0] и b[0] соответствовали следующим элементам массивов.

    В строке 12 происходит сравнение регистра, в котором хранится значение n, с нулём, чтобы узнать, нужно ли выполнять очередную итерацию цикла.

    В строке 14 происходит возврат из функции dot_product.

    Таким образом, данный код работает аналогично неоптимизированному, однако выполняет гораздо меньше "дорогих" обращений к памяти.

    Компиляция с флагом $$-O2$$

    dot_product:                            # @dot_product
    # %bb.0:
        beqz    a2, .LBB0_4
    # %bb.1:
        li  a3, 8
        andi    a6, a2, 7
        bgeu    a2, a3, .LBB0_5
    # %bb.2:
        fmv.w.x fa0, zero
        li  a2, 0
        bnez    a6, .LBB0_8
    .LBB0_3:
        ret
    .LBB0_4:
        fmv.w.x fa0, zero
        ret
    .LBB0_5:
        andi    a2, a2, -8
        fmv.w.x fa0, zero
        li  a4, 0
        neg a2, a2
        addi    a5, a1, 16
        addi    a3, a0, 16
    .LBB0_6:                                # =>This Inner Loop Header: Depth=1
        flw ft0, -16(a3)
        addi    a4, a4, -8
        flw ft1, -16(a5)
        flw ft2, -12(a5)
        fmadd.s ft0, ft0, ft1, fa0
        flw ft1, -12(a3)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, -8(a3)
        flw ft2, -8(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, -4(a3)
        flw ft2, -4(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 0(a3)
        flw ft2, 0(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 4(a3)
        flw ft2, 4(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 8(a3)
        flw ft2, 8(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 12(a3)
        flw ft2, 12(a5)
        addi    a5, a5, 32
        addi    a3, a3, 32
        fmadd.s fa0, ft1, ft2, ft0
        bne a2, a4, .LBB0_6
    # %bb.7:
        neg a2, a4
        beqz    a6, .LBB0_3
    .LBB0_8:
        slli    a2, a2, 2
        add a3, a0, a2
        flw ft0, 0(a3)
        add a3, a1, a2
        flw ft1, 0(a3)
        li  a3, 1
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.9:
        addi    a3, a2, 4
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 2
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.10:
        addi    a3, a2, 8
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 3
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.11:
        addi    a3, a2, 12
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 4
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.12:
        addi    a3, a2, 16
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 5
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.13:
        addi    a3, a2, 20
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 6
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.14:
        addi    a2, a2, 24
        add a0, a0, a2
        flw ft0, 0(a0)
        add a0, a1, a2
        flw ft1, 0(a0)
        fmadd.s fa0, ft0, ft1, fa0
        ret
    

    В данном случае порождается гораздо больше кода, чем при уровне оптимизаций -O1.

    Сначала проверяется случай n == 0 (строка 3), если это так, регистр, через который происходит возврат результата, инициализируется нулём и происходит возврат из функции dot_product (строки 15-16).

    Затем проверяется случай, когда n >= 8 (строки 5 и 7), если это не так (n < 8), происходит переход к строкам 57-117. Эти строки содержат 7 блоков, в каждом происходит вычисление r += a[i] * b[i] и проверяется, нужно ли закончить и вернуть результат.

    Если же n >= 8, то вычисления производятся блоками по 8 операций (строки 25-51):

    r += a[i + 0] * b[i + 0]; r += a[i + 1] * b[i + 1]; ... r += a[i + 7] * b[i + 7];
    

    То есть компилятор произвёл "раскрутку цикла". Важно отметить, что в таком случае проверять, нужно ли остановиться, достаточно один раз на весь блок (строка 52), а не на каждую операцию r += a[i] * b[i] . Это положительно сказывается на производительности, так как условные переходы-"дорогая" операция. В момент, когда осталось выполнить меньше 8 операций, проверяется, выполнены ли все вычисления (строки 54-55), если да, происходит возврат из функции dot_product (строка 13), иначе-происходит переход к случаю, когда надо вычислить менее 8 операций (строки 57-117).

    Поскольку блоки вычислений в строках 25-51 однотипны и данные лежат в памяти упорядоченно, вычисления могут быть векторизованы. Однако на момент создания этой лабораторной работы версия компилятора в Syntacore Kit не векторизует вычисления с числами с плавающей запятой.

    Немного изменим пример, чтобы продемонстрировать, как компилятор оптимизирует код с помощью векторных инструкций.

    Измените следующие строки в main.c:

  • 10-typedef int cell_t;
  • 43-printf("%i\n%.3g\n"
  • Чтобы получить векторные инструкции, необходимо указать векторное расширение в архитектуре, передав опцию -march=rv64gcv. Таким образом, получаем следующую команду:

    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main-O3v.s main.c -march=rv64gcv -O3
    

    $$Clang$$ с опциями $$-O3$$ и $$-march=rv64gcv$$

    dot_product:                            # @dot_product
    # %bb.0:
        beqz    a2, .LBB0_3
    # %bb.1:
        csrr    a3, vlenb
        srli    t0, a3, 1
        bgeu    a2, t0, .LBB0_4
    # %bb.2:
        li  a7, 0
        li  a3, 0
        j   .LBB0_7
    .LBB0_3:
        li  a0, 0
        ret
    .LBB0_4:
        addi    a4, t0, -1
        slli    t1, a3, 1
        and a6, a2, a4
        add t2, a0, a3
        add t3, a1, a3
        vsetvli a3, zero, e32, m1, ta, ma
        sub a7, a2, a6
        li  a5, 0
        vmv.v.i v8, 0
        mv  a3, a7
        vmv.v.i v9, 0
    .LBB0_5:                                # =>This Inner Loop Header: Depth=1
        add a4, a0, a5
        add t4, t2, a5
        vl1re32.v   v10, (a4)
        add a4, a1, a5
        vl1re32.v   v11, (a4)
        add a4, t3, a5
        vl1re32.v   v12, (t4)
        vl1re32.v   v13, (a4)
        sub a3, a3, t0
        add a5, a5, t1
        vmacc.vv    v8, v11, v10
        vmacc.vv    v9, v13, v12
        bnez    a3, .LBB0_5
    # %bb.6:
        vadd.vv v8, v9, v8
        vmv.s.x v9, zero
        vredsum.vs  v8, v8, v9
        vmv.x.s a3, v8
        beqz    a6, .LBB0_9
    .LBB0_7:
        slli    a4, a7, 2
        sub a2, a2, a7
        add a1, a1, a4
        add a0, a0, a4
    .LBB0_8:                                # =>This Inner Loop Header: Depth=1
        lw  a4, 0(a0)
        addi    a2, a2, -1
        lw  a5, 0(a1)
        addi    a1, a1, 4
        addi    a0, a0, 4
        mulw    a4, a5, a4
        addw    a3, a3, a4
        bnez    a2, .LBB0_8
    .LBB0_9:
        mv  a0, a3
        ret
    

    В данном ассемблерном коде инструкции и регистры, начинающиеся с буквы v относятся к векторному расширению (V) RISC?V.

    Сначала на основании соотношения длины векторных регистров в байтах и величины n принимается решение об использовании векторов (строки 5-7).

    Если векторы не используются, то вычисления производятся с помощью обычного цикла (строки 53-60).

    Если надо использовать векторы, то в 21 строке выставляется максимальная длина для используемых векторов, а в строках 24 и 26 инициализируются векторные регистры, в которых будет аккумулироваться вычисляемые значения. Далее в цикле происходят вычисления с использованием векторов (строки 28-40). После окончания цикла накопленные в векторных регистрах результаты складываются и записываются в обычный регистр (строки 42-45). Если ещё остались необработанные элементы исходных массивов, то они обрабатываются обычным циклом (строки 48-60).

    Попробуйте скомпилировать программу теми же опциями (-O3 и -march=rv64gcv), используя систему компиляции GCC.

    $$GCC$$ с опциями $$-O3$$ и $$-march=rv64gcv$$

    dot_product:
        beq a2,zero,.L4
        slli    a2,a2,2
        mv  a5,a0
        add a2,a0,a2
        li  a0,0
    .L3:
        lw  a3,0(a5)
        lw  a4,0(a1)
        addi    a5,a5,4
        addi    a1,a1,4
        mulw    a4,a4,a3
        addw    a0,a4,a0
        bne a2,a5,.L3
        ret
    .L4:
        li  a0,0
        ret
    

    GCC не породил векторизованный код. Различные системы компиляции имеют неодинаковую степень поддержки различных расширений RISC-V. Обратите на это внимание при выборе системы компиляции для своих проектов.

    Страницы:

    Данная лабораторная работа подготовлена Кириленко Яковом Александровичем по заказу Альянса RISC-V, допускается к использованию под лицензией CC BY 4.0.

    Приведённые ниже команды рассчитаны на выполнение в рабочей среде Syntacore Kit, распространяемой Альянсом RISC-V в образовательных целях. Обратите внимание, что в другом окружении приведённые примеры команд могут не работать или работать иначе.

    Рассмотрите программу, считающую скалярное произведение векторов.

    $$main.c$$

    #include <stdlib.h>
    #include <stdio.h>
    #include <memory.h>
    #include <time.h>
    #include <malloc.h>
    
    const size_t N = 200000;
    const size_t K = 2000;
    
    typedef  float cell_t;
    typedef cell_t * cell_ptr_t;
    
    cell_t dot_product(cell_ptr_t a, cell_ptr_t b, size_t n) {
        cell_t r = 0;
        for (size_t i = 0; i < n; ++i) {
            r += a[i] * b[i];
        }
        return r;
    }
    
    cell_ptr_t allocate() {
        cell_ptr_t r = (cell_t*) malloc(sizeof(cell_t) * N);
        for (size_t i = 1; i < N; ++i) {
            r[i] = (cell_t) (N *  rand() / RAND_MAX);
        }
        return r;
    }
    
    int main() {
        srand(0); // reproducible PRNG
        cell_ptr_t a = allocate();
        cell_ptr_t b = allocate();
        cell_t r1 = 0;
        clock_t t = clock();
        for (size_t i = 0; i < K; ++i) {
            int x = (t % (i + 1)); // some number to force dot_product call
            r1 += dot_product( (x  2) ? a : b
                            , (x  1) ? b : a
                            , N);
        }
        double time1 = (double)(clock()-t)/CLOCKS_PER_SEC;
    
        printf("%.3g\n%.3g\n"
            , r1
            , time1);
    
        free((void *)a);
        free((void *)b);
        return 0;
    }
    

    Кросс-компиляция и запуск

    Перед выполнением команд проверьте, определены ли переменные GCC_ROOT или RISCV и указан ли в переменной PATH путь /opt/syntacore/sc-dt/2023.08/tools/ (2023.08-версия Syntacore Kit):

    env | grep 'GCC_ROOT\|RISCV'
    echo $PATH
    

    Если нет, добавьте в файл ~/.bashrc следующие строки (пути зависят от версии используемой рабочей среды):

    Если переменая $$RISCV$$ не определена

    export GCC_ROOT="/opt/syntacore/sc-dt/2023.08/riscv-gcc"
    export PATH=$PATH:/opt/syntacore/sc-dt/2023.08/tools/bin
    

    Если переменая $$RISCV$$ определена

    export GCC_ROOT=$RISCV
    export PATH=$PATH:/opt/syntacore/sc-dt/2023.08/tools/bin
    

    Скомпилируйте программу под RISC-V. В Syntacore Kit для систем компиляции GCC и CLang это делается следующими способами:

    GCC CLang
    riscv64-unknown-linux-gnu-gcc main.c -o main
    
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -o main main.c
    

    Убедитесь, что полученный исполняемый файл предназначен для RISC?V:

    file ./main
    ./main: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, for GNU/Linux 4.15.0, not stripped
    

    Запустите скомпилированную программу в эмуляторе qemu:

    qemu-riscv64 -L "$GCC_ROOT/sysroot" ./main
    

    Отладка

    Скомпилируйте программу под RISC-V c отладочной информацией, выставив флаг -g:

    GCC CLang
    riscv64-unknown-linux-gnu-gcc -g main.c -o main
    
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -g -o main main.c
    

    Запустите qemu с сервером GDB на произвольном порту, например 12345:

    qemu-riscv64 -L $GCC_ROOT/sysroot -g 12345 ./main
    

    Он будет ждать подключения GDB. Затем запустите GDB из системы компиляции для RISC-V (с использованием отладочных символов из файла ./main):

    riscv64-unknown-linux-gnu-gdb ./main
    

    В консоли GDB подключитесь к GDB-серверу, запущенном в qemu:

    target remote localhost:12345
    

    Установите sysroot, чтобы GDB мог подгрузить символы используемых динамических библиотек:

    set sysroot /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot
    

    Затем выставьте точку останова:

    b main
    

    Продолжите выполнение программы:

    с
    

    Теперь можно отлаживать программу, запущенную в эмуляторе.

    Листинг консоли GDB после проделанных операций

    GNU gdb (GDB) 13.2
    Copyright (C) 2023 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.
    Type "show copying" and "show warranty" for details.
    This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-linux-gnu".
    Type "show configuration" for configuration details.
    For bug reporting instructions, please see:
    <https://www.gnu.org/software/gdb/bugs/>.
    Find the GDB manual and other documentation resources online at:
        <http://www.gnu.org/software/gdb/documentation/>.
    
    For help, type "help".
    Type "apropos word" to search for commands related to "word"...
    Reading symbols from ./main...
    (No debugging symbols found in ./main)
    (gdb) target remote localhost:12345
    Remote debugging using localhost:12345
    warning: remote target does not support file transfer, attempting to access files from local filesystem.
    warning: Unable to find dynamic linker breakpoint function.
    GDB will be unable to debug shared library initializers
    and track explicitly loaded dynamic code.
    0x00007f5ae4fdfb40 in ?? ()
    (gdb) set sysroot /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot
    Reading symbols from /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot/lib/ld-linux-riscv64-lp64d.so.1...
    (No debugging symbols found in /opt/syntacore/sc-dt/2023.08/riscv-gcc/sysroot/lib/ld-linux-riscv64-lp64d.so.1)
    (gdb) b main
    Breakpoint 1 at 0x1077e
    (gdb) c
    Continuing.
    
    Breakpoint 1, 0x000000000001077e in main ()
    (gdb)
    

    Оптимизация

    Рассмотрите листинги ассемблерного кода, полученные Clang при разных уровнях оптимизации, выполнив следующие команды:

  • без оптимизаций
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main.s main.c
    
  • с флагом -O1
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main-O1.s main.c -O1
    
  • с флагом -O2
    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main-O2.s main.c -O2
    
  • Будет получен следующий ассемблерный код для функции dot_product:

    Компиляция без оптимизаций

    dot_product:                            # @dot_product
    # %bb.0:
        addi    sp, sp, -64
        sd  ra, 56(sp)                      # 8-byte Folded Spill
        sd  s0, 48(sp)                      # 8-byte Folded Spill
        addi    s0, sp, 64
        sd  a0, -24(s0)
        sd  a1, -32(s0)
        sd  a2, -40(s0)
        li  a0, 0
        sw  a0, -44(s0)
        sd  a0, -56(s0)
        j   .LBB0_1
    .LBB0_1:                                # =>This Inner Loop Header: Depth=1
        ld  a0, -56(s0)
        ld  a1, -40(s0)
        bgeu    a0, a1, .LBB0_4
        j   .LBB0_2
    .LBB0_2:                                #   in Loop: Header=BB0_1 Depth=1
        ld  a0, -24(s0)
        ld  a1, -56(s0)
        slli    a1, a1, 2
        add a0, a0, a1
        flw ft0, 0(a0)
        ld  a0, -32(s0)
        add a0, a0, a1
        flw ft1, 0(a0)
        flw ft2, -44(s0)
        fmadd.s ft0, ft0, ft1, ft2
        fsw ft0, -44(s0)
        j   .LBB0_3
    .LBB0_3:                                #   in Loop: Header=BB0_1 Depth=1
        ld  a0, -56(s0)
        addi    a0, a0, 1
        sd  a0, -56(s0)
        j   .LBB0_1
    .LBB0_4:
        flw fa0, -44(s0)
        ld  ra, 56(sp)                      # 8-byte Folded Reload
        ld  s0, 48(sp)                      # 8-byte Folded Reload
        addi    sp, sp, 64
        ret
    

    В строках 3-12 происходит формирование кадра стека функции dot_product: выделяется необходимое для аргументов и локальных переменных место на стеке (строка 3), на стеке сохраняется адрес возврата и адрес предыдущего кадра (строки 4-5), в регистр сохраняется адрес текущего кадра (строка 6), переданные аргументы a, b и n загружаются на стек (строки 7-9), локальные переменные r и i инициализируются нулями (строки 10-12).

    В строках 15-17 вычисляется, нужно ли выполнять очередную итерацию цикла: со стека в регистры загружаются значения переменных i и n (строки 15-16), а затем сравниваются (строка 17).

    В строках 20-30 происходит вычисление очередной итерации цикла: со стека в регистр загружается значение переменной i (строка 21), вычисляются адреса в памяти значений a[i] и b[i] и они загружаются в регистры (строки 20, 22-27), со стека в регистр загружается значение переменной r (строка 28), к значению r прибавляется результат a[i] * b[i] (строка 29), новое значение r записывается на стек (строка 30).

    В строках 33-35 происходит увеличение счётчика цикла i после выполнения очередной итерации: со стека в регистр загружается значение переменной i (строка 33), значение переменной i увеличивается на 1 (строка 34), новое значение переменной i записывается на стек (строка 35).

    В строках 38-42 происходит возврат результата после выполнения цикла: со стека в регистр, через который возвращается результат, загружается значение переменной r (строка 38), со стека в регистры загружаются адрес возврата и адрес предыдущего кадра стека (строки 39-40), очищается кадр стека (строка 41), происходит возврат из функции dot_product (строка 42).

    Компиляция с флагом $$-O1$$

    dot_product:                            # @dot_product
    # %bb.0:
        fmv.w.x fa0, zero
        beqz    a2, .LBB0_2
    .LBB0_1:                                # =>This Inner Loop Header: Depth=1
        flw ft0, 0(a0)
        flw ft1, 0(a1)
        fmadd.s fa0, ft0, ft1, fa0
        addi    a2, a2, -1
        addi    a1, a1, 4
        addi    a0, a0, 4
        bnez    a2, .LBB0_1
    .LBB0_2:
        ret
    

    Полученный ассемблерный код значительно короче, полученного без применения оптимизаций. Это получается за счёт того, что на стеке не выделяется место под аргументы и локальные переменные, и все вычисления производятся с регистрами без обращений к памяти.

    В строке 3 происходит инициализация регистра, в котором хранится значение r, нулём.

    В строке 4 происходит сравнение регистра, в котором хранится значение n, с нулём, чтобы начать выполнение цикла.

    В строках 6-7 происходит загрузка в регистры значений a[0] и b[0].

    В строке 8 к значению r прибавляется результат a[0] * b[0].

    В строке 9 значение n уменьшается на 1.

    В строках 10-11 увеличиваются значения регистров, в которых хранятся адреса массивов a и b, чтобы на следующей итерации a[0] и b[0] соответствовали следующим элементам массивов.

    В строке 12 происходит сравнение регистра, в котором хранится значение n, с нулём, чтобы узнать, нужно ли выполнять очередную итерацию цикла.

    В строке 14 происходит возврат из функции dot_product.

    Таким образом, данный код работает аналогично неоптимизированному, однако выполняет гораздо меньше "дорогих" обращений к памяти.

    Компиляция с флагом $$-O2$$

    dot_product:                            # @dot_product
    # %bb.0:
        beqz    a2, .LBB0_4
    # %bb.1:
        li  a3, 8
        andi    a6, a2, 7
        bgeu    a2, a3, .LBB0_5
    # %bb.2:
        fmv.w.x fa0, zero
        li  a2, 0
        bnez    a6, .LBB0_8
    .LBB0_3:
        ret
    .LBB0_4:
        fmv.w.x fa0, zero
        ret
    .LBB0_5:
        andi    a2, a2, -8
        fmv.w.x fa0, zero
        li  a4, 0
        neg a2, a2
        addi    a5, a1, 16
        addi    a3, a0, 16
    .LBB0_6:                                # =>This Inner Loop Header: Depth=1
        flw ft0, -16(a3)
        addi    a4, a4, -8
        flw ft1, -16(a5)
        flw ft2, -12(a5)
        fmadd.s ft0, ft0, ft1, fa0
        flw ft1, -12(a3)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, -8(a3)
        flw ft2, -8(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, -4(a3)
        flw ft2, -4(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 0(a3)
        flw ft2, 0(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 4(a3)
        flw ft2, 4(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 8(a3)
        flw ft2, 8(a5)
        fmadd.s ft0, ft1, ft2, ft0
        flw ft1, 12(a3)
        flw ft2, 12(a5)
        addi    a5, a5, 32
        addi    a3, a3, 32
        fmadd.s fa0, ft1, ft2, ft0
        bne a2, a4, .LBB0_6
    # %bb.7:
        neg a2, a4
        beqz    a6, .LBB0_3
    .LBB0_8:
        slli    a2, a2, 2
        add a3, a0, a2
        flw ft0, 0(a3)
        add a3, a1, a2
        flw ft1, 0(a3)
        li  a3, 1
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.9:
        addi    a3, a2, 4
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 2
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.10:
        addi    a3, a2, 8
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 3
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.11:
        addi    a3, a2, 12
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 4
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.12:
        addi    a3, a2, 16
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 5
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.13:
        addi    a3, a2, 20
        add a4, a0, a3
        add a3, a3, a1
        flw ft1, 0(a3)
        li  a3, 6
        flw ft0, 0(a4)
        fmadd.s fa0, ft0, ft1, fa0
        beq a6, a3, .LBB0_3
    # %bb.14:
        addi    a2, a2, 24
        add a0, a0, a2
        flw ft0, 0(a0)
        add a0, a1, a2
        flw ft1, 0(a0)
        fmadd.s fa0, ft0, ft1, fa0
        ret
    

    В данном случае порождается гораздо больше кода, чем при уровне оптимизаций -O1.

    Сначала проверяется случай n == 0 (строка 3), если это так, регистр, через который происходит возврат результата, инициализируется нулём и происходит возврат из функции dot_product (строки 15-16).

    Затем проверяется случай, когда n >= 8 (строки 5 и 7), если это не так (n < 8), происходит переход к строкам 57-117. Эти строки содержат 7 блоков, в каждом происходит вычисление r += a[i] * b[i] и проверяется, нужно ли закончить и вернуть результат.

    Если же n >= 8, то вычисления производятся блоками по 8 операций (строки 25-51):

    r += a[i + 0] * b[i + 0]; r += a[i + 1] * b[i + 1]; ... r += a[i + 7] * b[i + 7];
    

    То есть компилятор произвёл "раскрутку цикла". Важно отметить, что в таком случае проверять, нужно ли остановиться, достаточно один раз на весь блок (строка 52), а не на каждую операцию r += a[i] * b[i] . Это положительно сказывается на производительности, так как условные переходы-"дорогая" операция. В момент, когда осталось выполнить меньше 8 операций, проверяется, выполнены ли все вычисления (строки 54-55), если да, происходит возврат из функции dot_product (строка 13), иначе-происходит переход к случаю, когда надо вычислить менее 8 операций (строки 57-117).

    Поскольку блоки вычислений в строках 25-51 однотипны и данные лежат в памяти упорядоченно, вычисления могут быть векторизованы. Однако на момент создания этой лабораторной работы версия компилятора в Syntacore Kit не векторизует вычисления с числами с плавающей запятой.

    Немного изменим пример, чтобы продемонстрировать, как компилятор оптимизирует код с помощью векторных инструкций.

    Измените следующие строки в main.c:

  • 10-typedef int cell_t;
  • 43-printf("%i\n%.3g\n"
  • Чтобы получить векторные инструкции, необходимо указать векторное расширение в архитектуре, передав опцию -march=rv64gcv. Таким образом, получаем следующую команду:

    clang -target riscv64-unknown-linux-gnu --sysroot="$GCC_ROOT/sysroot" --gcc-toolchain="$GCC_ROOT" -S -o main-O3v.s main.c -march=rv64gcv -O3
    

    $$Clang$$ с опциями $$-O3$$ и $$-march=rv64gcv$$

    dot_product:                            # @dot_product
    # %bb.0:
        beqz    a2, .LBB0_3
    # %bb.1:
        csrr    a3, vlenb
        srli    t0, a3, 1
        bgeu    a2, t0, .LBB0_4
    # %bb.2:
        li  a7, 0
        li  a3, 0
        j   .LBB0_7
    .LBB0_3:
        li  a0, 0
        ret
    .LBB0_4:
        addi    a4, t0, -1
        slli    t1, a3, 1
        and a6, a2, a4
        add t2, a0, a3
        add t3, a1, a3
        vsetvli a3, zero, e32, m1, ta, ma
        sub a7, a2, a6
        li  a5, 0
        vmv.v.i v8, 0
        mv  a3, a7
        vmv.v.i v9, 0
    .LBB0_5:                                # =>This Inner Loop Header: Depth=1
        add a4, a0, a5
        add t4, t2, a5
        vl1re32.v   v10, (a4)
        add a4, a1, a5
        vl1re32.v   v11, (a4)
        add a4, t3, a5
        vl1re32.v   v12, (t4)
        vl1re32.v   v13, (a4)
        sub a3, a3, t0
        add a5, a5, t1
        vmacc.vv    v8, v11, v10
        vmacc.vv    v9, v13, v12
        bnez    a3, .LBB0_5
    # %bb.6:
        vadd.vv v8, v9, v8
        vmv.s.x v9, zero
        vredsum.vs  v8, v8, v9
        vmv.x.s a3, v8
        beqz    a6, .LBB0_9
    .LBB0_7:
        slli    a4, a7, 2
        sub a2, a2, a7
        add a1, a1, a4
        add a0, a0, a4
    .LBB0_8:                                # =>This Inner Loop Header: Depth=1
        lw  a4, 0(a0)
        addi    a2, a2, -1
        lw  a5, 0(a1)
        addi    a1, a1, 4
        addi    a0, a0, 4
        mulw    a4, a5, a4
        addw    a3, a3, a4
        bnez    a2, .LBB0_8
    .LBB0_9:
        mv  a0, a3
        ret
    

    В данном ассемблерном коде инструкции и регистры, начинающиеся с буквы v относятся к векторному расширению (V) RISC?V.

    Сначала на основании соотношения длины векторных регистров в байтах и величины n принимается решение об использовании векторов (строки 5-7).

    Если векторы не используются, то вычисления производятся с помощью обычного цикла (строки 53-60).

    Если надо использовать векторы, то в 21 строке выставляется максимальная длина для используемых векторов, а в строках 24 и 26 инициализируются векторные регистры, в которых будет аккумулироваться вычисляемые значения. Далее в цикле происходят вычисления с использованием векторов (строки 28-40). После окончания цикла накопленные в векторных регистрах результаты складываются и записываются в обычный регистр (строки 42-45). Если ещё остались необработанные элементы исходных массивов, то они обрабатываются обычным циклом (строки 48-60).

    Попробуйте скомпилировать программу теми же опциями (-O3 и -march=rv64gcv), используя систему компиляции GCC.

    $$GCC$$ с опциями $$-O3$$ и $$-march=rv64gcv$$

    dot_product:
        beq a2,zero,.L4
        slli    a2,a2,2
        mv  a5,a0
        add a2,a0,a2
        li  a0,0
    .L3:
        lw  a3,0(a5)
        lw  a4,0(a1)
        addi    a5,a5,4
        addi    a1,a1,4
        mulw    a4,a4,a3
        addw    a0,a4,a0
        bne a2,a5,.L3
        ret
    .L4:
        li  a0,0
        ret
    

    GCC не породил векторизованный код. Различные системы компиляции имеют неодинаковую степень поддержки различных расширений RISC-V. Обратите на это внимание при выборе системы компиляции для своих проектов.

    Вернуться к учебному плану