Как получить доступ к элементам массива в встроенной сборке GCC? [Дубликат]

Это звучит как хорошая задача описания технологии W3C XPath . Легко выразить такие запросы, как «вернуть все href атрибуты в тегах img, которые вложены в elements». Не являясь баффом PHP, я не могу сказать вам, в какой форме XPath может быть доступен. Если вы можете вызвать внешнюю программу для обработки HTML-файла, вы сможете использовать версию командной строки XPath. Для быстрого ввода см. http://en.wikipedia.org/wiki/XPath .

4
задан Z boson 12 December 2015 в 21:39
поделиться

3 ответа

Избегайте 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 или операндов ввода / вывода памяти, чтобы он знал, какую память вы изменяете. "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 для этого и пару версий ниже.

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

Если вы хотите избежать «клонирования» памяти, вы можете использовать операнды ввода / вывода для фиктивной памяти, такие как "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 't use.

Вы можете использовать ограничение, которое сообщает gcc, что весь массив произвольной длины является входом или выходом: "m" (*(const struct {char a; char x[];} *) pStr) из ответ @David Wohlferd на 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's 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 было бы проще, но запись его таким образом делает меньшее изменение для раскомментации нагрузки в inline asm, вместо этого позволить компилятору получить одно значение в регистрах для нас.

3
ответ дан Peter Cordes 5 September 2018 в 09:35
поделиться

Когда я компилирую ваш код add_asm2 с gcc (4.9.2), я получаю:

add_asm2:
.LFB0:
        .cfi_startproc
        xorl        %eax, %eax
        xorl        %r8d, %r8d
        testl       %ecx, %ecx
        je  .L1
        .p2align 4,,10
        .p2align 3
.L5:
#APP
# 3 "add_asm2.c" 1
        movaps   (%rsi,%rax), %xmm0
addps    (%rdi,%rax), %xmm0
movaps   %xmm0, (%rdx,%rax)

# 0 "" 2
#NO_APP
        addl        $4, %r8d
        addq        $16, %rax
        cmpl        %r8d, %ecx
        ja  .L5
.L1:
        rep; ret
        .cfi_endproc

, поэтому он не идеален (он использует резервный регистр), но использует индексированные нагрузки ...

2
ответ дан Chris Dodd 5 September 2018 в 09:35
поделиться

gcc также имеет встроенные векторные расширения , которые являются даже кросс-платформой:

typedef float v4sf __attribute__((vector_size(16)));
void add_vector(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n/4; i+=1) {
        *(v4sf*)(z + 4*i) = *(v4sf*)(x + 4*i) + *(v4sf*)(y + 4*i);
    }
}

В моей версии gcc 4.7.2 сгенерированная сборка:

.L28:
        movaps  (%rdi,%rax), %xmm0
        addps   (%rsi,%rax), %xmm0
        movaps  %xmm0, (%rdx,%rax)
        addq    $16, %rax
        cmpq    %rcx, %rax
        jne     .L28
2
ответ дан Jester 5 September 2018 в 09:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: