Если вы используете временные переменные Си и выходные операнды для всех живых регистров в первой половине, которые совпадают с входными ограничениями для второй половины, вы сможете разделить его на свой встроенный ассемблер без потери производительности, особенно если вы используетеопределенные ограничения ввода / вывода памяти вместо всеобъемлющего "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.)
См. Также Зацикливание массивов с помощью встроенной сборки