RGBA для ABGR: встроенный кронштейн neon asm для iOS/XCode

Этот код (очень похожий код, не пробовал именно этот код) компилируется с помощью Android NDK, но не с XCode/armv7 + arm64/iOS

Ошибки в комментариях:

uint32_t *src;
uint32_t *dst;

#ifdef __ARM_NEON
__asm__ volatile(
    "vld1.32 {d0, d1}, [%[src]] \n" // error: Vector register expected
    "vrev32.8 q0, q0            \n" // error: Unrecognized instruction mnemonic
    "vst1.32 {d0, d1}, [%[dst]] \n" // error: Vector register expected
    :
    : [src]"r"(src), [dst]"r"(dst)
    : "d0", "d1"
    );
#endif

Что не так с этим кодом?

EDIT1:

Я переписал код, используя встроенные функции:

uint8x16_t x = vreinterpretq_u8_u32(vld1q_u32(src));
uint8x16_t y = vrev32q_u8(x);
vst1q_u32(dst, vreinterpretq_u32_u8(y));

После разборки я получаю следующее, которое является вариантом, который я уже пробовал:

vld1.32 {d16, d17}, [r0]!
vrev32.8    q8, q8
vst1.32 {d16, d17}, [r1]!

Итак, мой код выглядит так же, но дает те же ошибки:

__asm__ volatile("vld1.32 {d0, d1}, [%0]! \n"
                 "vrev32.8 q0, q0         \n"
                 "vst1.32 {d0, d1}, [%1]! \n"
                 :
                 : "r"(src), "r"(dst)
                 : "d0", "d1"
                 );

EDIT2:

Чтение через разборку, я действительно нашел вторую версию функции. Оказывается, arm64 использует немного другой набор команд. Например, сборка arm64 использует rev32.16b v0, v0 вместо этого. Список функций (которые я не могу сделать головами или хвостами) ниже:

_My_Function:
cmp     w2, #0
add w9, w2, #3
csel    w8, w9, w2, lt
cmp     w9, #7
b.lo    0x3f4
asr w9, w8, #2
ldr     x8, [x0]
mov  w9, w9
lsl x9, x9, #2
ldr q0, [x8], #16
rev32.16b   v0, v0
str q0, [x1], #16
sub x9, x9, #16
cbnz    x9, 0x3e0
ret

Ответы

Ответ 1

Как указано в изменениях к исходному вопросу, оказалось, что мне нужна другая реализация сборки для arm64 и armv7.

#ifdef __ARM_NEON
  #if __LP64__
asm volatile("ldr q0, [%0], #16  \n"
             "rev32.16b v0, v0   \n"
             "str q0, [%1], #16  \n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1"
             );
  #else
asm volatile("vld1.32 {d0, d1}, [%0]! \n"
             "vrev32.8 q0, q0         \n"
             "vst1.32 {d0, d1}, [%1]! \n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1"
             );
  #endif
#else

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

Ответ 2

Я успешно опубликовал несколько приложений для iOS, которые используют язык ассемблера ARM, а встроенный код - самый неприятный способ сделать это. Apple по-прежнему требует, чтобы приложения поддерживали устройства ARM32 и ARM64. Поскольку код будет построен как ARM32 и ARM64 по умолчанию (если вы не изменили параметры компиляции), вам нужно разработать код, который будет успешно компилироваться в обоих режимах. Как вы заметили, ARM64 - это совершенно другой мнемонический формат и модель регистров. Существует два простых способа:

1) Напишите свой код с помощью NEON. ARM указала, что исходные свойства ARM32 останутся в основном неизменными для целей ARMv8 и поэтому могут быть скомпилированы как для ARM32, так и для ARM64-кода. Это самый безопасный/простой вариант.

2) Запишите встроенный код или отдельный модуль .S для вашего кода языка ассемблера. Чтобы справиться с 2 режимами компиляции, используйте "#ifdef __arm64__" и "#ifdef __arm__", чтобы различать два набора инструкций.

Ответ 3

Intrinsics, по-видимому, единственный способ использовать один и тот же код для NEON между ARM (32-разрядной версией) и Aarch64.

Есть много причин не использовать : https://gcc.gnu.org/wiki/DontUseInlineAsm

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

Официальная документация ARM:

Хотя технически возможно оптимизировать сборку NEON вручную, это может быть очень сложно, поскольку тайм-ауты доступа к конвейеру и памяти имеют сложные взаимозависимости. Вместо ручной сборки ARM настоятельно рекомендует использовать встроенные функции


Если вы все равно используете inline asm, избегайте будущей боли, правильно поняв.

Легко писать встроенный asm, который работает, но не безопасен. будущие изменения источника (а иногда и будущие оптимизации компилятора), потому что ограничения не точно описывают, что делает asm. Симптомы будут странными, и подобная контекстно-зависимая ошибка может даже привести к прохождению единичных тестов, но неправильный код в основной программе. (или наоборот).

Скрытая ошибка, которая не вызывает каких-либо дефектов в текущей сборке, по-прежнему является ошибкой и является действительно Bad Thing в ответе Stackoverflow, который можно скопировать в качестве примера в другие контексты. @bitwise код в вопросе и самоответ, оба имеют такие ошибки.

Встроенный asm в вопросе небезопасен, так как он изменяет память, сообщающую компилятору об этом. Вероятно, это проявляется только в цикле, который читается из dst в C как до, так и после inline asm. Тем не менее, это легко исправить, и это позволяет нам сбросить volatile (и "клобук" памяти "памяти", который он отсутствует), чтобы компилятор мог лучше оптимизировать (но все еще со значительными ограничениями по сравнению с внутренними). ​​

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


Следующие компиляции для ARM и Aarch64. Использование -funroll-loops приводит к тому, что gcc выбирает разные режимы адресации и не заставляет dst++; src++; выполняться между каждым оператором asm asm. (Возможно, это было бы невозможно с помощью asm volatile).

Я использовал операнды памяти, поэтому компилятор знает, что память - это вход и вывод, а дает компилятору возможность использовать автоматическую добавочную/декрементирующую адресацию режимы. Это лучше, чем все, что вы можете сделать с указателем в регистре в качестве входного операнда, потому что он позволяет работать с циклом.

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

void bytereverse32(uint32_t *dst32, const uint32_t *src32, size_t len)
{
    typedef struct { uint64_t low, high; } vec128_t;
    const vec128_t *src = (const vec128_t*) src32;
    vec128_t *dst = (vec128_t*) dst32;

    // with old gcc, this gets gcc to use a pointer compare as the loop condition
    // instead of incrementing a loop counter
    const vec128_t *src_endp = src + len/(sizeof(vec128_t)/sizeof(uint32_t));
    // len is in units of 4-byte chunks

    while (src < src_endp) {

        #if defined(__ARM_NEON__) || defined(__ARM_NEON)
          #if __LP64__
        // aarch64 registers: s0 and d0 are subsets of q0 (128bit), synonym for v0
        asm ("ldr        q0, %[src] \n\t"
             "rev32.16b  v0, v0 \n\t"
             "str        q0, %[dst]  \n\t"
                     : [dst] "=<>m"(*dst)  // auto-increment/decrement or "normal" memory operand
                     : [src] "<>m" (*src)
                     : "q0", "v0"
                     );
          #else
        // arm32 registers: 128bit q0 is made of d0:d1, or s0:s3
        asm ("vld1.32   {d0, d1}, %[src] \n\t"
             "vrev32.8   q0, q0          \n\t"  // reverse 8 bit elements inside 32bit words
             "vst1.32   {d0, d1}, %[dst] \n"
                     : [dst] "=<>m"(*dst)
                     : [src] "<>m"(*src)
                     : "d0", "d1"
                     );
          #endif
        #else
         #error "no NEON"
        #endif

      // increment pointers by 16 bytes
        src++;   // The inline asm doesn't modify the pointers.
        dst++;   // of course, these increments may compile to a post-increment addressing mode
                 // this way has the advantage of letting the compiler unroll or whatever

     }
}

Это компилирует (в Godbolt explorer с gcc 4.8), но я не знаю, собирается ли он, не говоря уже о работе правильно, Тем не менее, я уверен, что эти ограничения операндов верны. Ограничения в основном одинаковы для всех архитектур, и я понимаю их намного лучше, чем я знаю NEON.

Во всяком случае, внутренний цикл на ARM (32 бит) с gcc 4.8 -O3, без -funroll-loops:

.L4:
    vld1.32   {d0, d1}, [r1], #16   @ MEM[(const struct vec128_t *)src32_17]
    vrev32.8   q0, q0          
    vst1.32   {d0, d1}, [r0], #16   @ MEM[(struct vec128_t *)dst32_18]

    cmp     r3, r1    @ src_endp, src32
    bhi     .L4       @,

Ошибка ограничения регистра

В коде в автоответчике OP есть другая ошибка: операнды указателя ввода используют отдельные ограничения "r". Это приводит к поломке, если компилятор хочет сохранить старое значение вокруг и выбирает входной регистр для src, который не совпадает с выходным регистром.

Если вы хотите вводить указатели в регистры и выбирать свои собственные режимы адресации, вы можете использовать "0" соответствия-ограничения, или вы можете использовать операторы вывода "+r" read-write.

Вам также понадобятся операнды ввода/вывода типа clobber или dummy "memory" (то есть, которые сообщают компилятору, какие байты памяти считываются и записываются, даже если вы не используете этот номер операнда в inline asm).

См. Цитирование по массивам с помощью встроенной сборкидля обсуждения преимуществ и недостатков использования ограничений r для циклизации по массиву на x86. ARM имеет режимы адресации с автоматическим увеличением, которые, как представляется, создают лучший код, чем все, что вы можете получить с помощью ручного выбора режимов адресации. Он позволяет gcc использовать разные режимы адресации в разных копиях блока при разворачивании цикла. "r" (pointer), как представляется, не имеют преимуществ, поэтому я не буду подробно разбираться в том, как использовать фиктивное ограничение ввода/вывода, чтобы избежать необходимости использования clobber "memory".


Тестирование, которое генерирует неправильный код с помощью оператора @bitwise asm:

// return a value as a way to tell the compiler it needed after
uint32_t* unsafe_asm(uint32_t *dst, const uint32_t *src)
{
  uint32_t *orig_dst = dst;

  uint32_t initial_dst0val = orig_dst[0];
#ifdef __ARM_NEON
  #if __LP64__
asm volatile("ldr q0, [%0], #16   # unused src input was %2\n\t"
             "rev32.16b v0, v0   \n\t"
             "str q0, [%1], #16   # unused dst input was %3\n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1"  // ,"memory"
               // clobbers don't include v0?
            );
  #else
asm volatile("vld1.32 {d0, d1}, [%0]!  # unused src input was %2\n\t"
             "vrev32.8 q0, q0         \n\t"
             "vst1.32 {d0, d1}, [%1]!  # unused dst input was %3\n"
             : "=r"(src), "=r"(dst)
             : "r"(src), "r"(dst)
             : "d0", "d1" // ,"memory"
             );
  #endif
#else
    #error "No NEON/AdvSIMD"
#endif

  uint32_t final_dst0val = orig_dst[0];
  // gcc assumes the asm doesn't change orig_dst[0], so it only does one load (after the asm)
  // and uses it for final and initial
  // uncomment the memory clobber, or use a dummy output operand, to avoid this.
  // pointer + initial+final compiles to LSL 3 to multiply by 8 = 2 * sizeof(uint32_t)


  // using orig_dst after the inline asm makes the compiler choose different registers for the
  // "=r"(dst) output operand and the "r"(dst) input operand, since the asm constraints
  // advertise this non-destructive capability.
  return orig_dst + final_dst0val + initial_dst0val;
}

Скомпилируется (AArch64 gcc4.8 -O3):

    ldr q0, [x1], #16   # unused src input was x1   // src, src
    rev32.16b v0, v0   
    str q0, [x2], #16   # unused dst input was x0   // dst, dst

    ldr     w1, [x0]  // D.2576, *dst_1(D)
    add     x0, x0, x1, lsl 3 //, dst, D.2576,
    ret

В хранилище используется x2 (неинициализированный регистр, так как эта функция принимает только 2 аргумента). Выход "=r"(dst) (% 1) выбрал x2, а вход "r"(dst) (% 3, который используется только в комментарии) выбрал x0.

final_dst0val + initial_dst0val компилируется в 2x final_dst0val, потому что мы лгали компилятору и сказали ему, что память не была изменена. Поэтому вместо того, чтобы читать одну и ту же память до и после оператора inline asm, он просто читает после и слева сдвигает одну дополнительную позицию при добавлении к указателю. (Возвращаемое значение существует только для использования значений, чтобы они не оптимизировались).

Мы можем исправить обе проблемы, исправляя ограничения: используя "+r" для указателей и добавляя clobber "memory". (Выход фиктивной работы также будет работать, и это может повредить оптимизации.) Я не беспокоился, так как это, похоже, не имеет преимуществ по сравнению с версией операнда памяти выше.

С этими изменениями мы получаем

safe_register_pointer_asm:
    ldr     w3, [x0]  //, *dst_1(D)
    mov     x2, x0    // dst, dst    ### These 2 insns are new

    ldr q0, [x1], #16       // src
    rev32.16b v0, v0   
    str q0, [x2], #16       // dst

    ldr     w1, [x0]  // D.2597, *dst_1(D)
    add     x3, x1, x3, uxtw  // D.2597, D.2597, initial_dst0val   ## And this is new, to add the before and after loads
    add     x0, x0, x3, lsl 2 //, dst, D.2597,
    ret
safe_register_pointer_asm:
    ldr     w3, [x0]  //, *dst_1(D)
    mov     x2, x0    // dst, dst    ### These 2 insns are new

    ldr q0, [x1], #16       // src
    rev32.16b v0, v0   
    str q0, [x2], #16       // dst

    ldr     w1, [x0]  // D.2597, *dst_1(D)
    add     x3, x1, x3, uxtw  // D.2597, D.2597, initial_dst0val   ## And this is new, to add the before and after loads
    add     x0, x0, x3, lsl 2 //, dst, D.2597,
    ret