Как использовать шаблон C ++ для условной компиляции кода ASM? - PullRequest
0 голосов
/ 15 февраля 2019

Существует переменная bool с именем «Enable», когда «Enable» имеет значение false, я хочу создать следующую функцию:

void test_false()
{
   float dst[4] = {1.0, 1.0, 1.0, 1.0};
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   float * src_addr = src;


   asm volatile (
                 "vld1.32    {q0}, [%[src]]  \n"
                 "vld1.32    {q1}, [%[dst]]  \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vst1.32    {q0}, [%[dst]]  \n"
                 :[src]"+r"(src_addr),
                 [dst]"+r"(dst_addr)
                 :
                 : "q0", "q1", "q2", "q3", "memory"
                 );

   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }
}

А когда «Enable» имеет значение true, я хочу создать следующую функцию:

void test_true()
{
   float dst[4] = {1.0, 1.0, 1.0, 1.0};
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   float * src_addr = src;


   asm volatile (
                 "vld1.32    {q0}, [%[src]]  \n"
                 "vld1.32    {q1}, [%[dst]]  \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vadd.f32   q0, q0, q1      \n" //Only here is different from test_false()
                 "vst1.32    {q0}, [%[dst]]  \n"
                 :[src]"+r"(src_addr),
                 [dst]"+r"(dst_addr)
                 :
                 : "q0", "q1", "q2", "q3", "memory"
                 );

   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }
}

Но я не хочу сохранять две копии кода, потому что большинство из них одинаковы.Я хочу использовать «c ++ Template + Conditional Compile» для решения моей проблемы.Код выглядит следующим образом.Но это не сработало.Независимо от того, является ли Enable истинным или ложным, компилятор создает код, аналогичный test_true ().

template<bool Enable>
void test_tmp()
{
   float dst[4] = {1.0, 1.0, 1.0, 1.0};
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   float * src_addr = src;

    if (Enable)
    {
        #define FUSE_
    }

   asm volatile (
                 "vld1.32    {q0}, [%[src]]  \n"
                 "vld1.32    {q1}, [%[dst]]  \n"
                 "vadd.f32   q0, q0, q1          \n"
                 "vadd.f32   q0, q0, q1          \n"

                 #ifdef FUSE_
                 "vadd.f32   q0, q0, q1          \n"
                 #endif

                 "vst1.32    {q0}, [%[dst]]  \n"
                 :[src]"+r"(src_addr),
                 [dst]"+r"(dst_addr)
                 :
                 : "q0", "q1", "q2", "q3", "memory"
                 );



   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }

   #undef FUSE_
}


template void test_tmp<true>();
template void test_tmp<false>();

Кажется невозможным написать код, подобный функции test_tmp ().Кто-нибудь знает, как решить мою проблему?Большое спасибо.

Ответы [ 2 ]

0 голосов
/ 15 февраля 2019

Если вы используете временные переменные Си и выходные операнды для всех живых регистров в первой половине, которые совпадают с входными ограничениями для второй половины, вы сможете разделить его на свой встроенный ассемблер без потери производительности, особенно если вы используетеопределенные ограничения ввода / вывода памяти вместо всеобъемлющего "memory" clobber.Но это будет намного сложнее.


Это, очевидно, не работает, потому что препроцессор C запускает до того, как компилятор C ++ даже смотрит на if() операторы.

if (Enable) {
    #define FUSE_    // always defined, regardless of Enable
}

Но ассемблер GNU имеет свои собственные директивы макро / условной сборки, такие как .if, которые работают с ассемблером, который компилятор выдает после подстановки текста в шаблон asm(), включая фактические числовые значения для немедленного вводаоперанды.

Используйте bool в качестве входного операнда для директивы ассемблера .if

Используйте ограничение ввода "i" (Enable).Обычно расширение %0 или %[enable] этого будет #0 или #1, потому что именно так печатается ARM немедленно.Но в GCC есть модификатор %c0 / %c[enable], который печатает константу без знаков препинания.(Это задокументировано для x86 , но работает точно так же для ARM и, вероятно, для всех других архитектур. Документация для модификаторов операндов ARM / AArch64 находится в разработке; я сидел в электронном письме об этом ...)

".if %c[enable] \n\t" для [enable] "i" (c_var) заменит .if 0 или .if 1 в шаблон inline-asm, именно то, что нам нужно сделать .if / .endifработа во время сборки.

Полный пример:

template<bool Enable>
void test_tmp(float dst[4])
{
   //float dst[4] = {1.0, 1.0, 1.0, 1.0};
   // static const    // non-static-const so we can see the memory clobber vs. dummy src stop this from optimizing away init of src[] on the stack
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   const float * src_addr = src;

   asm (
                 "vld1.32    {q1}, [%[dst]]  @ dummy dst = %[dummy_memdst]\n" // hopefully they pick the same regs?
                 "vld1.32    {q0}, [%[src]]  @ dummy src = %[dummy_memsrc]\n"
                 "vadd.f32   q0, q0, q1          \n"  // TODO: optimize to q1+q1 first, without a dep on src
                 "vadd.f32   q0, q0, q1          \n"  // allowing q0+=q1 and q1+=q1 in parallel if we need q0 += 3*q1
//                 #ifdef FUSE_
                ".if %c[enable]\n"    // %c modifier: print constant without punctuation, same as documented for x86
                 "vadd.f32   q0, q0, q1          \n"
                 ".endif \n"
//                 #endif

                 "vst1.32    {q0}, [%[dst]]  \n"
                 : [dummy_memdst] "+m" (*(float(*)[4])dst_addr)
                 : [src]"r"(src_addr),
                   [dst]"r"(dst_addr),
                   [enable]"i"(Enable)
                  , [dummy_memsrc] "m" (*(const float(*)[4])src_addr)
                 : "q0", "q1", "q2", "q3" //, "memory"
                 );


/*
   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }
*/
}

float dst[4] = {1.0, 1.0, 1.0, 1.0};
template void test_tmp<true>(float *);
template void test_tmp<false>(float *);

компилируется с GCC и Clang в проводнике компилятора Godbolt

С gcc вы толькополучите вывод компилятора .s, так что вы должны отключить некоторые из обычных фильтров компилятора и просмотреть директивы.Все инструкции 3 vadd.f32 присутствуют в версии false, но одна из них окружена .if 0 / .endif.

Но встроенный ассемблер clang обрабатывает директивы ассемблера внутри, прежде чем повернуть назадв asm, если этот вывод запрашивается.(Обычно clang / LLVM идет прямо к машинному коду, в отличие от gcc, который всегда запускает отдельный ассемблер).

Просто для ясности, это работает с gcc и clang, но это проще сделатьувидеть это на Godbolt с лязгом.(Потому что у Godbolt нет «бинарного» режима, который фактически собирает, а затем разбирает, кроме x86). Вывод Clang для false версии

 ...

    vld1.32 {d2, d3}, [r0]    @ dummy dst = [r0]
    vld1.32 {d0, d1}, [r1]    @ dummy src = [r1]
    vadd.f32        q0, q0, q1
    vadd.f32        q0, q0, q1
    vst1.32 {d0, d1}, [r0]

 ... 

Обратите внимание, что clang выбрал тот же регистр GP для необработанных указателей, который использовался для операнда памяти.(gcc, кажется, выбирает [sp] для src_mem, но другой регистр для ввода указателя, который вы используете вручную в режиме адресации).Если бы вы не заставили его указатели в регистрах, он мог бы использовать режим адресации с относительной SP со смещением для векторных нагрузок, потенциально используя преимущества режимов адресации ARM.

Если выв действительности, не собираясь изменять указатели внутри asm (например, с режимами адресации после приращения), тогда "r" операнды только для ввода имеют смысл.Если бы мы оставили цикл printf, компилятору снова потребовалось бы dst после asm, поэтому было бы полезно иметь его все еще в регистре.Вход "+r"(dst_addr) заставляет компилятор предполагать, что этот регистр больше не может использоваться как копия dst.В любом случае, gcc всегда копирует регистры, даже если это позже не нужно, делаю ли я это "r" или "+r", так что это странно.

Использование (пустышка)Входы / выходы памяти означают, что мы можем отбросить volatile, поэтому компилятор может оптимизировать его как обычную функцию от своих входов.(И оптимизировать его, если результат не используется.)

Надеюсь, это не хуже кодекса, чем с "memory" clobber.Но, вероятно, было бы лучше, если бы вы просто использовали операнды памяти "=m" и "m" и вообще не запрашивали указатели в регистрах.(Это не поможет, если вы собираетесь зацикливать массив с помощью встроенного asm.)

См. Также Зацикливание массивов с помощью встроенной сборки

0 голосов
/ 15 февраля 2019

Я не занимался сборкой ARM в течение нескольких лет, и мне никогда не удавалось должным образом изучить встроенную сборку GCC, но я думаю, что ваш код можно переписать так, используя встроенные функции:

#include <cstdio>
#include <arm_neon.h>

template<bool Enable>
void test_tmp()
{
    const float32x4_t src = {1.0, 2.0, 3.0, 4.0};
    const float32x4_t src2 = {1.0, 1.0, 1.0, 1.0};
    float32x4_t z;

    z = vaddq_f32(src, src2);
    z = vaddq_f32(z, src2);
    if (Enable) z = vaddq_f32(z, src2);
    float result[4];
    vst1q_f32(result, z);
    for (int i = 0; i < 4; i++)
    {
        printf("%f, ", result[i]);//0.0  0.0  0.0  0.0
    }
}

template void test_tmp<true>();
template void test_tmp<false>();

Вы можете увидеть полученный машинный код + игрушку в прямом эфире по адресу: https://godbolt.org/z/Fg7Tci

Скомпилировано с ARM gcc8.2 и параметрами командной строки "-O3 -mfloat-abi = softfp -mfpu = neon" the "true "вариант таков:

void test_tmp<true>():
        vmov.f32        q9, #1.0e+0  @ v4sf
        vldr    d16, .L6
        vldr    d17, .L6+8
        # and the FALSE variant has one less vadd.f32 in this part
        vadd.f32        q8, q8, q9
        vadd.f32        q8, q8, q9
        vadd.f32        q8, q8, q9
        push    {r4, r5, r6, lr}
        sub     sp, sp, #16
        vst1.32 {d16-d17}, [sp:64]
        mov     r4, sp
        ldr     r5, .L6+16
        add     r6, sp, #16
.L2:
        vldmia.32       r4!, {s15}
        vcvt.f64.f32    d16, s15
        mov     r0, r5
        vmov    r2, r3, d16
        bl      printf
        cmp     r4, r6
        bne     .L2
        add     sp, sp, #16
        pop     {r4, r5, r6, pc}

.L6:
        .word   1065353216
        .word   1073741824
        .word   1077936128
        .word   1082130432
        .word   .LC0

.LC0:
        .ascii  "%f, \000"

Это все еще оставляет меня в замешательстве из-за того, что gcc не просто вычисляет конечную строку со значениями в качестве строки для вывода, поскольку входные данные являются постоянными.Может быть, какое-то математическое правило о точности не позволяет сделать это во время компиляции, поскольку результат может незначительно отличаться от фактического целевого HU платформы HW платформы?Т.е. с некоторым быстрым математическим переключателем он, вероятно, полностью отбросит этот код и просто выдаст одну строку вывода ...

Но я полагаю, что ваш код на самом деле не соответствует "MCVE" тому, что вы делаете, и тестузначения будут передаваться в какую-то реальную функцию, которую вы тестируете, или что-то в этом роде.

В любом случае, если вы работаете над оптимизацией performance , вам, скорее всего, следует полностью избегать встроенной сборки и использовать встроенные функциивместо этого, поскольку это позволяет компилятору лучше распределять регистры и оптимизировать код вокруг вычислений (я не отслеживал это точно, но я думаю, что последняя версия этого эксперимента в godbolt была на 2-4 инструкции короче / проще, чем оригинал с использованием inlineсборка).

Кроме того, вы избежите неправильных ограничений asm, как в вашем примере кода, их всегда сложно получить правильно и поддерживать PITA, если вы продолжаете часто модифицировать встроенный код.

...