Этот код (очень похожий код, не пробовал именно этот код) компилируется с помощью Android NDK, но не с XCode/armv7 + arm64/iOS
После разборки я получаю следующее, которое является вариантом, который я уже пробовал:
Чтение через разборку, я действительно нашел вторую версию функции. Оказывается, arm64 использует немного другой набор команд. Например, сборка arm64 использует rev32.16b v0, v0
вместо этого. Список функций (которые я не могу сделать головами или хвостами) ниже:
Ответ 3
Intrinsics, по-видимому, единственный способ использовать один и тот же код для NEON между ARM (32-разрядной версией) и Aarch64.
Есть много причин не использовать inline-assembly: 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