Ускорение умножения матричных векторов с помощью ARM Neon Intrinsics на Raspberry Pi 4

Мне нужно оптимизировать умножение матричного вектора. Данные выглядят следующим образом:

  • Вектор имеет 81 столбец
  • Матрица имеет 90 000 строк и 81 столбец и уже транспонирована. Таким образом, можно использовать скалярное произведение по строкам.
  • Таким образом, на выходе получается вектор с 90 000 строк.
  • Все лежат в одномерном массиве с плавающей запятой

Для этой подпрограммы также должны быть выполнены некоторые нефункциональные требования:

  • Следует использовать как можно меньше стандартных библиотек (например, без std::vector)
  • Никакая сторонняя библиотека не должна использоваться (так что для меня тоже нет Eigen или Blas)

Это мой (упрощенный, где я предполагаю, что ввод полностью заблокирован для удобочитаемости) код,

// input_height = 90000
// input_width = 81

for (uint32_t y = 0; y < input_height; y += 4) {
    float32x4_t sum0 = vmovq_n_f32(0);
    float32x4_t sum1 = vmovq_n_f32(0);
    float32x4_t sum2 = vmovq_n_f32(0);
    float32x4_t sum3 = vmovq_n_f32(0);

    for (uint32_t x = 0; x < input_width; x += 16) {
        float32x4x4_t A = load_matrix_transpose(kernel + x);

        float32x4x4_t B0 = load_matrix_transpose(input + y * input_width + x);
        float32x4x4_t B1 = load_matrix_transpose(input + (y + 1) * input_width + x);
        float32x4x4_t B2 = load_matrix_transpose(input + (y + 2) * input_width + x);
        float32x4x4_t B3 = load_matrix_transpose(input + (y + 3) * input_width + x);

        matrix_element_wise_multiplication(A, B0, sum0);
        matrix_element_wise_multiplication(A, B1, sum1);
        matrix_element_wise_multiplication(A, B2, sum2);
        matrix_element_wise_multiplication(A, B3, sum3);
    }

    output[y] = vaddvq_f32(sum0);
    output[y + 1] = vaddvq_f32(sum1);
    output[y + 2] = vaddvq_f32(sum2);
    output[y + 3] = vaddvq_f32(sum3);
}

Где load_matrix_transpose, matrix_element_wise_multiplication — это следующие функции:

inline float32x4x4_t load_matrix_transpose(float *a) {
    float32x4x4_t ret;

    ret.val[0] = simd_load(a);

    ret.val[1] = simd_load(a + 4);

    ret.val[2] = simd_load(a + 8);

    ret.val[3] = simd_load(a + 12);

    return ret;
}

inline void simd_matrix_element_wise_multiplication(float32x4x4_t & A, float32x4x4_t & B, float32x4x4_t & C) {
    C = vmlaq_f32(C, A.val[0], B.val[0]);
    C = vmlaq_f32(C, A.val[1], B.val[1]);
    C = vmlaq_f32(C, A.val[2], B.val[2]);
    C = vmlaq_f32(C, A.val[3], B.val[3]);
}

На моем Rasperry Pi 4 (ARMv8, 8 ГБ ОЗУ, 4 ядра) код берет с уровнем оптимизации -O3 около 60ms.

В долгосрочной перспективе (много циклов) версия регистра Neon ровно в два раза быстрее, чем обычный код.

Мой вопрос в том, есть ли способ дальнейшей оптимизации кода? Я пробовал много вещей, но не могу ничего улучшить по сравнению с обычным кодом.


person Long Nguyen    schedule 16.03.2021    source источник
comment
Вы сравнивали это с такой библиотекой, как Eigen? (компиляция с -O2 -march=native, опционально также с -omp)   -  person chtz    schedule 17.03.2021
comment
@chtz, это хорошее предложение. Если бы производительность была где-то рядом с Eigen (в чем я сомневаюсь), то я бы знал, что дальнейшая оптимизация невозможна.   -  person Long Nguyen    schedule 17.03.2021
comment
Зачем транспонировать матрицу? На маленьких матрицах (4x4) имеет смысл избегать трудоемкого 2xvpadd, но на такой большой матрице (81x90000) оно уменьшается. И транспонирование матрицы не бесплатно.   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
Вам намного лучше с 90000xdot произведением 81.   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
@ Jake'Alquimista'LEE Почему матрица транспонируется? эта матрица уже транспонирована при появлении   -  person Long Nguyen    schedule 17.03.2021
comment
@ Jake'Alquimista'LEE, так что вместо 16 умножений на 4 строки я должен делать 64 умножения на одну строку?   -  person Long Nguyen    schedule 17.03.2021
comment
Я не знаю насчет emergence Можете ли вы отключить транспонирование? Кстати, нет абсолютно никаких причин для использования векторных типовx4, если только вы не используете типы vld4/vst4 x4, предназначенные для последовательных регистров, и вы никогда не знаете, сколько еще компиляторов беспорядка будет генерировать из-за этого.   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
@ Jake'Alquimista'LEE Я не могу отключить транспонирование, так как матрица уже пришла ко мне такой. Так что транспонирование мне тоже ничего не стоит. Кстати, нет абсолютно никакой причины использовать векторные типы x4, если только вы не используете vld4/vst4. Типы x4 предназначены для последовательных регистров, и вы никогда не знаете, сколько еще беспорядка будут генерировать компиляторы из-за этого. Это хороший совет, я удалю float32x4x4_t   -  person Long Nguyen    schedule 17.03.2021
comment
Это очень плохо. Он будет работать на порядок быстрее с отключенным транспонированием.   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
@Jake'Alquimista'LEE Как это? Я мог бы написать другую версию матричной функции, чтобы вернуть мне нетранспонированный результат. Это займет у меня время, но ваше решение звучит очень многообещающе.   -  person Long Nguyen    schedule 17.03.2021
comment
Я только что проверил свою функцию: на RK3368 (Cortex-A53) требуется 1,4 мс.   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
@Jake'Alquimista'LEE черт возьми, это больно. Я переписываю свой код, в данный момент....   -  person Long Nguyen    schedule 17.03.2021
comment
Хорошо, я опубликую ответ в течение нескольких часов после анализа разборки.   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
Я исправил глупую ошибку, и это занимает 10,4 мс   -  person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
@Jake'Alquimista'LEE, это было бы еще лучше   -  person Long Nguyen    schedule 17.03.2021


Ответы (2)


Локальность данных является наивысшим приоритетом, когда дело доходит до оптимизации, и вы должны знать о емкости регистров, поскольку регистры БЕЗУМНО являются самым быстрым и дефицитным ресурсом.

aarch64: 32x128-битных neon регистров (512 байтов)
aarch32: 16x128-битных neon регистров (256 байтов)

Матрица 81 x 90 000 при транспонировании требует хранения 90 000 промежуточных значений для выполнения умножения, а поскольку 360 000 байтов не помещаются в банк регистров размером 512 байт, потребуется ТОННА подкачки памяти, что приводит к ОГРОМНЫМ ударам по производительности.
С другой стороны, 4*81 байт вектора прекрасно вписываются в 512 байт.

void matVecMult81x90000(float *pDst, float *pMat, float *pVec)
{
    register float32x4_t vec0_3, vec4_7, vec8_11, vec12_15, vec16_19, vec20_23, vec24_27, vec28_31, vec32_35, vec36_39, vec40_43, vec44_47, vec48_51, vec52_55, vec56_59, vec60_63, vec64_67, vec68_71, vec72_75, vec76_79, vec80;
    register float32x4_t mat0, mat1, mat2, mat3, mat4, rslt;
    register float32x2_t drslt;
    register uint32_t nRows = 90000;

    vec80 = vdupq_n_f32(0.0f);
    mat4 =vdupq_n_f32(0.0f);
    vec0_3 = vld1q_f32(pVec); pVec += 4;
    vec4_7 = vld1q_f32(pVec); pVec += 4;
    vec8_11 = vld1q_f32(pVec); pVec += 4;
    vec12_15 = vld1q_f32(pVec); pVec += 4;
    vec16_19 = vld1q_f32(pVec); pVec += 4;
    vec20_23 = vld1q_f32(pVec); pVec += 4;
    vec24_27 = vld1q_f32(pVec); pVec += 4;
    vec28_31 = vld1q_f32(pVec); pVec += 4;
    vec32_35 = vld1q_f32(pVec); pVec += 4;
    vec36_39 = vld1q_f32(pVec); pVec += 4;
    vec40_43 = vld1q_f32(pVec); pVec += 4;
    vec44_47 = vld1q_f32(pVec); pVec += 4;
    vec48_51 = vld1q_f32(pVec); pVec += 4;
    vec52_55 = vld1q_f32(pVec); pVec += 4;
    vec56_59 = vld1q_f32(pVec); pVec += 4;
    vec60_63 = vld1q_f32(pVec); pVec += 4;
    vec64_67 = vld1q_f32(pVec); pVec += 4;
    vec68_71 = vld1q_f32(pVec); pVec += 4;
    vec72_75 = vld1q_f32(pVec); pVec += 4;
    vec76_79 = vld1q_f32(pVec); pVec += 4;
    vld1q_lane_f32(pVec, vec80, 0);

    do {
        mat0 = vld1q_f32(pMat); pMat += 4;
        mat1 = vld1q_f32(pMat); pMat += 4;
        mat2 = vld1q_f32(pMat); pMat += 4;
        mat3 = vld1q_f32(pMat); pMat += 4;
        rslt = vmulq_f32(mat0, vec0_3);
        rslt += vmulq_f32(mat1, vec4_7);
        rslt += vmulq_f32(mat2, vec8_11);
        rslt += vmulq_f32(mat3, vec12_15);

        mat0 = vld1q_f32(pMat); pMat += 4;
        mat1 = vld1q_f32(pMat); pMat += 4;
        mat2 = vld1q_f32(pMat); pMat += 4;
        mat3 = vld1q_f32(pMat); pMat += 4;
        rslt += vmulq_f32(mat0, vec16_19);
        rslt += vmulq_f32(mat1, vec20_23);
        rslt += vmulq_f32(mat2, vec24_27);
        rslt += vmulq_f32(mat3, vec28_31);

        mat0 = vld1q_f32(pMat); pMat += 4;
        mat1 = vld1q_f32(pMat); pMat += 4;
        mat2 = vld1q_f32(pMat); pMat += 4;
        mat3 = vld1q_f32(pMat); pMat += 4;
        rslt += vmulq_f32(mat0, vec32_35);
        rslt += vmulq_f32(mat1, vec36_39);
        rslt += vmulq_f32(mat2, vec40_43);
        rslt += vmulq_f32(mat3, vec44_47);

        mat0 = vld1q_f32(pMat); pMat += 4;
        mat1 = vld1q_f32(pMat); pMat += 4;
        mat2 = vld1q_f32(pMat); pMat += 4;
        mat3 = vld1q_f32(pMat); pMat += 4;
        rslt += vmulq_f32(mat0, vec48_51);
        rslt += vmulq_f32(mat1, vec52_55);
        rslt += vmulq_f32(mat2, vec56_59);
        rslt += vmulq_f32(mat3, vec60_63);

        mat0 = vld1q_f32(pMat); pMat += 4;
        mat1 = vld1q_f32(pMat); pMat += 4;
        mat2 = vld1q_f32(pMat); pMat += 4;
        mat3 = vld1q_f32(pMat); pMat += 4;
        vld1q_lane_f32(pMat, mat4, 0); pMat += 1;
        rslt += vmulq_f32(mat0, vec64_67);
        rslt += vmulq_f32(mat1, vec68_71);
        rslt += vmulq_f32(mat2, vec72_75);
        rslt += vmulq_f32(mat3, vec76_79);
        rslt += vmulq_f32(mat4, vec80);

        *pDst++ = vaddvq_f32(rslt);
    } while (--nRows);
}

К сожалению, компиляторы не умеют подыгрывать. (И GCC, и Clang)
Сгенерированный код показывает некоторую замену стека на Vector внутри цикла. Ниже приведена та же функция в написанном от руки ассемблере без замены стека:

    .arch   armv8-a
    .global     matVecMult81x90000_asm
    .text

.balign 64
.func
matVecMult81x90000_asm:
// init loop counter
    mov     w3, #90000 & 0xffff
    movk    w3, #90000>>16, lsl #16

// preserve registers
    stp     d8, d9, [sp, #-48]!
    stp     d10, d11, [sp, #1*16]
    stp     d12, d13, [sp, #2*16]

// load vectors
    ldp     q0, q1, [x2, #0*32]
    ldp     q2, q3, [x2, #1*32]
    ldp     q4, q5, [x2, #2*32]
    ldp     q6, q7, [x2, #3*32]
    ldp     q8, q9, [x2, #4*32]
    ldp     q10, q11, [x2, #5*32]
    ldp     q12, q13, [x2, #6*32]
    ldp     q16, q17, [x2, #7*32]
    ldp     q18, q19, [x2, #8*32]
    ldp     q20, q21, [x2, #9*32]
    ldr     s22, [x2, #10*32]

// loop
.balign 64
1:
    ldp     q24, q25, [x1, #0*32]
    ldp     q26, q27, [x1, #1*32]
    ldp     q28, q29, [x1, #2*32]
    ldp     q30, q31, [x1, #3*32]
    subs    w3, w3, #1

    fmul    v23.4s, v24.4s, v0.4s
    fmla    v23.4s, v25.4s, v1.4s
    fmla    v23.4s, v26.4s, v2.4s
    fmla    v23.4s, v27.4s, v3.4s
    fmla    v23.4s, v28.4s, v4.4s
    fmla    v23.4s, v29.4s, v5.4s
    fmla    v23.4s, v30.4s, v6.4s
    fmla    v23.4s, v31.4s, v7.4s

    ldp     q24, q25, [x1, #4*32]
    ldp     q26, q27, [x1, #5*32]
    ldp     q28, q29, [x1, #6*32]
    ldp     q30, q31, [x1, #7*32]

    fmla    v23.4s, v24.4s, v8.4s
    fmla    v23.4s, v25.4s, v9.4s
    fmla    v23.4s, v26.4s, v10.4s
    fmla    v23.4s, v27.4s, v11.4s
    fmla    v23.4s, v28.4s, v12.4s
    fmla    v23.4s, v29.4s, v13.4s
    fmla    v23.4s, v30.4s, v16.4s
    fmla    v23.4s, v31.4s, v17.4s

    ldp     q24, q25, [x1, #8*32]
    ldp     q26, q27, [x1, #9*32]
    ldr     s28, [x1, #10*32]

    fmla    v23.4s, v24.4s, v18.4s
    fmla    v23.4s, v25.4s, v19.4s
    fmla    v23.4s, v26.4s, v20.4s
    fmla    v23.4s, v27.4s, v21.4s
    fmla    v23.4s, v28.4s, v22.4s

    add     x1, x1, #81*4

    faddp   v23.4s, v23.4s, v23.4s
    faddp   v23.2s, v23.2s, v23.2s

    str     s23, [x0], #4
    b.ne    1b

.balign 8
//restore registers

    ldp     d10, d11, [sp, #1*16]
    ldp     d12, d13, [sp, #2*16]
    ldp     d8, d9, [sp], #48

// return
    ret

.endfunc
.end

Результаты тестирования на RK3368:
встроенные компоненты Clang: 10,41 мс
сборка: 9,59 мс

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

person Jake 'Alquimista' LEE    schedule 17.03.2021
comment
Спасибо, я попробую ваш код и вернусь после тестирования - person Long Nguyen; 17.03.2021
comment
От 10 секунд до 4 секунд. Удивительно. Спасибо большое - person Long Nguyen; 17.03.2021
comment
Чтобы было ясно. Наивная версия занимает 21 секунду, ваша — 4 секунды. Это почти в 5 раз быстрее. Я не тороплюсь и изучаю сборку. - person Long Nguyen; 17.03.2021
comment
Основным недостатком вашей версии является один аккумулятор, эти строки rslt += создают цепочку зависимостей. Я переработал ваш код, чтобы использовать 4 из них: gist.github.com/Const-me/ c7b80e991b93f8a2d9ac6fb0fd9db878 - person Soonts; 17.03.2021
comment
Кстати, GCC 10 показал себя неплохо, я не думаю, что для этого вам нужна сборка: godbolt.org/z/ ajex8e - person Soonts; 17.03.2021
comment
@Soonts, не могли бы вы также написать свой код в качестве ответа на этот вопрос? - person Long Nguyen; 18.03.2021
comment
@LongNguyen Хорошо, добавил еще один. - person Soonts; 18.03.2021
comment
@soonts - VFMLA - это слитое умножение с накоплением. Я сомневаюсь, что точность останется прежней, если вы используете несколько промежуточных значений; - mul и mla могут выполняться подряд без задержки; - ядра, работающие по порядку, в любом случае не могут выполнять два умножения одновременно, и цепочка зависимостей не является проблемой для ядер, работающих не по порядку, по крайней мере, в этом случае. - person Jake 'Alquimista' LEE; 18.03.2021
comment
@Jake'Alquimista'LEE Независимые аккумуляторы повышают точность суммирования. Лучший способ обычно en.wikipedia.org/wiki/Pairwise_summation и 16 аккумуляторов - это шаг в правильном направлении, чуть лучше, чем 4 аккумулятора (1 вектор) в вашем варианте. Посмотрите руководство по оптимизации, на которое я ссылался в своем ответе, в нем говорится, что на ЦП OP задержка FMA составляет 7 циклов, но пропускная способность составляет 1 цикл, это означает, что довольно много инструкций в полете могут выполняться параллельно, если только зависимости данных не сериализуют их. Интересно, как производительность сравнивается на практике. У меня Pi4, но я использую 32-битную ОС. - person Soonts; 18.03.2021
comment
@soonts Да, мне уже давно это интересно. Возможно, я проверю это в ближайшее время. - person Jake 'Alquimista' LEE; 18.03.2021
comment
@soonts ссылка, которую вы дали, не принимает во внимание FMA. Необходимость добавить 16 значений с плавающей запятой неточно вредит больше, чем только 4. И я повторяю: менее мощные ядра с порядком, такие как Cortex-A53, могут выполнять только одну инструкцию умножения за цикл для начала, а задержки обнуляются при выполнении mlas снова и снова. Вот что значит спина к спине. - person Jake 'Alquimista' LEE; 18.03.2021
comment
Загрузите PDF-файл и перейдите на страницу 28. Pi 4 имеет Cortex A72, а не A53. Задержки никогда не обнуляются, это фундаментальные ограничения чипа. Такой код, как r+=a*b; r+=c*d; r+=e*f; r+=g*h;, может выполнять только один MLA каждые 3 цикла (зависимость данных от аккумулятора), узкие места в задержке. Такой код, как r1+=a*b; r2+=c*d; r3+=e*f; r4+=g*h;, может выполнять один MLA за такт, потому что эти инструкции независимы, а пропускная способность составляет 1 такт. - person Soonts; 18.03.2021
comment
Вы не получите 3-кратной разницы, потому что там есть не только вычислительные нагрузки, но я ожидаю измеримой разницы. - person Soonts; 18.03.2021
comment
У меня постепенно возникает ощущение, что вы не знаете, что FUSED многократно накапливается. Использование нескольких аккумуляторов — не вариант для начала. И я очень сомневаюсь в других вещах, которые вы продолжаете рассказывать. Вы можете дать мне тысячи ссылок на спецификации. Знаешь что? О БПФ ОПУБЛИКОВАНО ТОННА, и большинство из них устарели, потому что современные архитектуры ведут себя совсем по-другому. Сначала узнайте о FUSED, умножьте накопление и измерьте скорость и точность. Тогда вы можете поговорить со мной. - person Jake 'Alquimista' LEE; 18.03.2021

Вот оптимизация ответа Джейка.

Использование 4 аккумуляторов вместо одного помогает, потому что инструкции FMA имеют задержку, намного превышающую пропускную способность. Согласно руководству по оптимизации Cortex-A72, задержка FMLA инструкции составляет 7 циклов для полная вещь или 3 цикла, когда зависимость находится на аккумуляторе (если вам интересно, что, черт возьми, Q-форма и D-форма, Q для 16-байтовых векторов, D для 8-байтовых векторов). Пропускная способность намного выше, это 1 такт, процессор может запускать один FMA за каждый такт.

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

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

void matVecMult81( float *pDst, const float *pMat, const float *pVec, size_t nRows = 90000 )
{
    // 30 vector registers in total; ARM64 has 32 of them, so we're good.
    float32x4_t vec0_3, vec4_7, vec8_11, vec12_15, vec16_19, vec20_23, vec24_27, vec28_31, vec32_35, vec36_39, vec40_43, vec44_47, vec48_51, vec52_55, vec56_59, vec60_63, vec64_67, vec68_71, vec72_75, vec76_79, vec80;
    float32x4_t mat0, mat1, mat2, mat3, mat4;
    float32x4_t res0, res1, res2, res3;

    vec80 = mat4 = vdupq_n_f32( 0.0f );
    // Load 16 numbers from pVec into 3 vector registers, incrementing the source pointer
#define LOAD_VEC_16( v0, v1, v2, v3 )      \
    v0 = vld1q_f32( pVec ); pVec += 4;     \
    v1 = vld1q_f32( pVec ); pVec += 4;     \
    v2 = vld1q_f32( pVec ); pVec += 4;     \
    v3 = vld1q_f32( pVec ); pVec += 4

    // Load the complete vector into registers using the above macro
    LOAD_VEC_16( vec0_3, vec4_7, vec8_11, vec12_15 );
    LOAD_VEC_16( vec16_19, vec20_23, vec24_27, vec28_31 );
    LOAD_VEC_16( vec32_35, vec36_39, vec40_43, vec44_47 );
    LOAD_VEC_16( vec48_51, vec52_55, vec56_59, vec60_63 );
    LOAD_VEC_16( vec64_67, vec68_71, vec72_75, vec76_79 );
    // Load the final scalar of the vector
    vec80 = vld1q_lane_f32( pVec, vec80, 0 );

#undef LOAD_VEC_16

    // Load 16 numbers from pMat into mat0 - mat3, incrementing the source pointer
#define LOAD_MATRIX_16()                         \
        mat0 = vld1q_f32( pMat ); pMat += 4;     \
        mat1 = vld1q_f32( pMat ); pMat += 4;     \
        mat2 = vld1q_f32( pMat ); pMat += 4;     \
        mat3 = vld1q_f32( pMat ); pMat += 4

    // Multiply 16 numbers in mat0 - mat3 by the specified pieces of the vector, and accumulate into res0 - res3
    // Multiple accumulators is critical for performance, 4 instructions produced by this macro don't have data dependencies between them.
#define HANDLE_BLOCK_16( v0, v1, v2, v3 )        \
        res0 = vfmaq_f32( res0, mat0, v0 );      \
        res1 = vfmaq_f32( res1, mat1, v1 );      \
        res2 = vfmaq_f32( res2, mat2, v2 );      \
        res3 = vfmaq_f32( res3, mat3, v3 )

    const float* const pMatEnd = pMat + nRows * 81;
    while( pMat < pMatEnd )
    {
        // Initial 16 elements only need multiplication.
        LOAD_MATRIX_16();
        res0 = vmulq_f32( mat0, vec0_3 );
        res1 = vmulq_f32( mat1, vec4_7 );
        res2 = vmulq_f32( mat2, vec8_11 );
        res3 = vmulq_f32( mat3, vec12_15 );

        // Handle the rest of the row using FMA instructions.
        LOAD_MATRIX_16();
        HANDLE_BLOCK_16( vec16_19, vec20_23, vec24_27, vec28_31 );

        LOAD_MATRIX_16();
        HANDLE_BLOCK_16( vec32_35, vec36_39, vec40_43, vec44_47 );

        LOAD_MATRIX_16();
        HANDLE_BLOCK_16( vec48_51, vec52_55, vec56_59, vec60_63 );

        // The final block of the row has 17 scalars instead of 16
        LOAD_MATRIX_16();
        mat4 = vld1q_lane_f32( pMat, mat4, 0 ); pMat++;

        HANDLE_BLOCK_16( vec64_67, vec68_71, vec72_75, vec76_79 );
        res0 = vfmaq_f32( res0, mat4, vec80 );

        // Vertically add 4 accumulators into res0
        res1 = vaddq_f32( res1, res2 );
        res0 = vaddq_f32( res3, res0 );
        res0 = vaddq_f32( res1, res0 );

        // Store the horizontal sum of the accumulator
        *pDst = vaddvq_f32( res0 );
        pDst++;
    }

#undef LOAD_MATRIX_16
#undef HANDLE_BLOCK_16
}

Сборка, сгенерированная из этого источника с помощью GCC 10.1, выглядит более или менее нормально.

person Soonts    schedule 17.03.2021
comment
Комментарии не для расширенного обсуждения; этот разговор был перемещен в чат< /а>. - person Machavity♦; 19.03.2021