Ответ 1
Избегайте inline asm, когда это возможно: https://gcc.gnu.org/wiki/DontUseInlineAsm. Он блокирует множество оптимизаций. Но если вы действительно не можете удержать компилятор в создании asm, который вы хотите, вы должны, вероятно, написать весь цикл в asm, чтобы вы могли развернуть и настроить его вручную, вместо того чтобы делать такие вещи.
Вы можете использовать ограничение r
для индекса. Используйте модификатор q
, чтобы получить имя 64-битного регистра, чтобы вы могли использовать его в режиме адресации. При компиляции для 32-битных целей модификатор q
выбирает имя 32-битного регистра, поэтому тот же код все еще работает.
Если вы хотите выбрать, какой режим адресации используется, вам нужно сделать это самостоятельно, используя операнды указателя с ограничениями r
.
Синтаксис inline asm GNU C не предполагает, что вы читаете или записываете память, на которую указывают операнды указателя. (например, возможно, вы используете inline-asm and
по значению указателя). Поэтому вам нужно что-то сделать с помощью "memory"
clobber или операндов ввода/вывода памяти, чтобы он знал, какую память вы изменяете. A "memory"
clobber прост, но заставляет все, кроме локальных, проливать/перезагружать. См. Раздел Clobbers в документах для примера использования фиктивного входного операнда.
Еще одно огромное преимущество для ограничения m
заключается в том, что -funroll-loops
может работать, генерируя адреса с постоянными смещениями. Выполнение адресации не позволяет компилятору выполнить один приращение каждые 4 итерации или что-то еще, потому что каждое значение исходного уровня i
должно появиться в регистре.
Здесь моя версия, с некоторыми настройками, как указано в комментариях.
#include <immintrin.h>
void add_asm1_memclobber(float *x, float *y, float *z, unsigned n) {
__m128 vectmp; // let the compiler choose a scratch register
for(int i=0; i<n; i+=4) {
__asm__ __volatile__ (
"movaps (%[y],%q[idx],4), %[vectmp]\n\t" // q modifier: 64bit version of a GP reg
"addps (%[x],%q[idx],4), %[vectmp]\n\t"
"movaps %[vectmp], (%[z],%q[idx],4)\n\t"
: [vectmp] "=x" (vectmp) // "=m" (z[i]) // gives worse code if the compiler prepares a reg we don't use
: [z] "r" (z), [y] "r" (y), [x] "r" (x),
[idx] "r" (i) // unrolling is impossible this way (without an insn for every increment by 4)
: "memory"
// you can avoid a "memory" clobber with dummy input/output operands
);
}
}
Godbolt compiler explorer asm для этого и пару версий ниже.
Ваша версия должна объявить %xmm0
как clobbered, или у вас будет плохое время, когда это будет включено. Моя версия использует временную переменную как операнд только для вывода, который никогда не использовался. Это дает компилятору полную свободу для размещения регистров.
Если вы хотите избежать "клонирования" памяти, вы можете использовать операнды ввода/вывода логической памяти, такие как "m" (*(const __m128*)&x[i])
, чтобы сообщить компилятору, какая память читается и записывается вашей функцией. Это необходимо для обеспечения правильного генерации кода, если вы сделали что-то вроде x[4] = 1.0;
прямо перед запуском этого цикла. (И даже если вы не пишете то, что простое, вложение и постоянное распространение могут сводиться к этому.) А также чтобы убедиться, что компилятор не читает с z[]
до того, как цикл будет запущен.
В этом случае мы получаем ужасные результаты: gcc5.x фактически увеличивает 3 дополнительных указателя, потому что он решает использовать режимы адресации [reg]
вместо индексирования. Он не знает, что inline asm никогда не ссылается на эти операнды памяти, используя режим адресации, созданный ограничением!
# gcc5.4 with dummy constraints like "=m" (*(__m128*)&z[i]) instead of "memory" clobber
.L11:
movaps (%rsi,%rax,4), %xmm0 # y, i, vectmp
addps (%rdi,%rax,4), %xmm0 # x, i, vectmp
movaps %xmm0, (%rdx,%rax,4) # vectmp, z, i
addl $4, %eax #, i
addq $16, %r10 #, ivtmp.19
addq $16, %r9 #, ivtmp.21
addq $16, %r8 #, ivtmp.22
cmpl %eax, %ecx # i, n
ja .L11 #,
r8, r9 и r10 - дополнительные указатели, которые не используют встроенный блок asm.
Вы можете использовать ограничение, которое сообщает gcc, что весь массив произвольной длины является входом или выходом: "m" (*(const struct {char a; char x[];} *) pStr)
from @Ответ Дэвида Вольферда на asm strlen
. Поскольку мы хотим использовать индексированные режимы адресации, у нас будет базовый адрес всех трех массивов в регистрах, и эта форма ограничения запрашивает базовый адрес как операнд, а не указатель на текущую память, на которой работает.
Это фактически работает без дополнительных приращений счетчика внутри цикла:
void add_asm1_dummy_whole_array(const float *restrict x, const float *restrict y,
float *restrict z, unsigned n) {
__m128 vectmp; // let the compiler choose a scratch register
for(int i=0; i<n; i+=4) {
__asm__ __volatile__ (
"movaps (%[y],%q[idx],4), %[vectmp]\n\t" // q modifier: 64bit version of a GP reg
"addps (%[x],%q[idx],4), %[vectmp]\n\t"
"movaps %[vectmp], (%[z],%q[idx],4)\n\t"
: [vectmp] "=x" (vectmp) // "=m" (z[i]) // gives worse code if the compiler prepares a reg we don't use
, "=m" (*(struct {float a; float x[];} *) z)
: [z] "r" (z), [y] "r" (y), [x] "r" (x),
[idx] "r" (i) // unrolling is impossible this way (without an insn for every increment by 4)
, "m" (*(const struct {float a; float x[];} *) x),
"m" (*(const struct {float a; float x[];} *) y)
);
}
}
Это дает нам тот же внутренний цикл, который мы получили с clobber "memory"
:
.L19: # with clobbers like "m" (*(const struct {float a; float x[];} *) y)
movaps (%rsi,%rax,4), %xmm0 # y, i, vectmp
addps (%rdi,%rax,4), %xmm0 # x, i, vectmp
movaps %xmm0, (%rdx,%rax,4) # vectmp, z, i
addl $4, %eax #, i
cmpl %eax, %ecx # i, n
ja .L19 #,
Он сообщает компилятору, что каждый блок asm считывает или записывает все массивы, поэтому он может необоснованно останавливать его от чередования с другим кодом (например, после полной развертки с низким числом итераций). Он не останавливает разворачивание, но требование иметь каждое значение индекса в регистре делает его менее эффективным.
Версия с ограничениями m
, что gcc может развернуть:
#include <immintrin.h>
void add_asm1(float *x, float *y, float *z, unsigned n) {
__m128 vectmp; // let the compiler choose a scratch register
for(int i=0; i<n; i+=4) {
__asm__ __volatile__ (
// "movaps %[yi], %[vectmp]\n\t"
"addps %[xi], %[vectmp]\n\t" // We requested that the %[yi] input be in the same register as the [vectmp] dummy output
"movaps %[vectmp], %[zi]\n\t"
// ugly ugly type-punning casts; __m128 is a may_alias type so it safe.
: [vectmp] "=x" (vectmp), [zi] "=m" (*(__m128*)&z[i])
: [yi] "0" (*(__m128*)&y[i]) // or [yi] "xm" (*(__m128*)&y[i]), and uncomment the movaps load
, [xi] "xm" (*(__m128*)&x[i])
: // memory clobber not needed
);
}
}
Использование [yi]
в качестве операнда ввода/вывода +x
было бы проще, но запись этого способа делает меньшее изменение для раскомментации нагрузки в встроенном asm вместо того, чтобы позволить компилятору получить одно значение для регистров для нас.