Опубликован: 24.11.2024 | Доступ: свободный | Студентов: 1 / 0 | Длительность: 02:06:00
Лекция 7:

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

< Лекция 6 || Лекция 7: 123

В данном случае порождается гораздо больше кода, чем при уровне оптимизаций -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. Обратите на это внимание при выборе системы компиляции для своих проектов.

< Лекция 6 || Лекция 7: 123