dodawanie dwóch wektorów w assembly

0

Witam
Napisałem sobie prostą strukturę wektora i funkcję która za pomocą assembly będzie dodawała dwa wektory do siebie

struct Vectors {
    float x1, x2, x3, x4;
};

struct Vectors dodawanie(const struct Vectors v1, const struct Vectors v2) {
    struct Vectors vec;
    
    asm
    (
        "ADDPS %%XMM0, %%XMM1 \n"
        //""
        :"=xmm"(&vec)               //wyjście
        :"xmm"(&v1, &v2)           //wejście
        :"%xmm0", "xmm1"        //używane rejestry
     );
    
    return vec;
}

I właśnie w tej linii ":"=xmm"(&vec) " coś się krzaczy. Robię na podstawie tego: http://wiki.osdev.org/Inline_Assembly
Możliwe że dla struktur robi się jakoś inaczej. Proszę o jakieś podpowiedzi.
Pozdrawiam!

1

Pisanie od zera w asm-ie (na dodatek w inline assembly ze składnią AT&T) jest delikatnie mówiąc hardkorowe. O wiele łatwiej wektoryzuje się
w C z użyciem odpowiednich intrinsics-ów. Dlatego najlepiej wyjść od działającej wersji w C (nawet w przypadku tak prostego kodu jak tutaj)
a dopiero później po obejrzeniu kodu wynikowego przygotować wstawkę w asemblerze. W dalszej części celowo użyję składni Intela bo IMO jest ona czytelniejsza.

  1. Kod w C z użyciem intrinsics-ów.
 
#define EQUAL(a, b) (fabs((a) - (b)) < 0.0001f)

struct __attribute__ ((aligned (16))) Vector
{
    float x1, x2, x3, x4;
};

static void intrinsics_sum()
{
    volatile unsigned iterations = 1;

    struct Vector v1 = { 1.0f, 4.0f, 9.0f, 16.0f };
    struct Vector v2 = { 2.0f, 8.0f, 18.0f, 32.0f };
    struct Vector out = { 0.0f, 0.0f, 0.0f, 0.0f };

    __m128 *ptr1 = (__m128 *)&v1;
    __m128 *ptr2 = (__m128 *)&v2;
    float *outptr = (float *)&out;

    for (unsigned i = 0; i < iterations; i++)
    {
        _mm_store_ps(outptr, _mm_add_ps(*ptr1, *ptr2));
        outptr += 4;
        ptr1++;
        ptr2++;
    }
    assert( EQUAL(out.x1, 3.0f) &&  EQUAL(out.x2, 12.0f)
           && EQUAL(out.x3, 27.0f) && EQUAL(out.x4, 48.0f));
}

Uwagi do funkcji intrinsics_sum:

  • _mm_add_ps generuje instrukcję addps z SSE która dodaje dwa wektory float-ów i umieszcza w trzecim wektorze
  • _mm_store_ps generuje instrukcję movaps z SSE przenoszącą wynik dodawania z rejestru do pamięci
  • struktura Vector musi mieć 16B alignment, inaczej nie ma szans na wygenerowanie optymalnego kodu
  • zmienna iterations musi być oznaczona jako volatile, w innym wypadku kompilator wykona sumowanie w czasie kompilacji
  • porównywanie float-ów jest wykonane przez EQUAL z niewielkim episilonem
  1. Teraz skompilujemy kod z optymalizacjami oraz wygenerujemy dump-a ze składnią Intela.
gcc main.c -Ofast -o main
objdump --no-show-raw-insn -d -M intel -S main > dump.asm

Listing jest krótki. Można dość łatwo zlokalizować ciało pętli z interesującymi nas instrukcjami SSE.
U mnie wygląda to tak:

   mov    ecx,DWORD PTR [rsp+0xc]           ;
   add    edx,0x1                           ; i++
   movaps xmm0,XMMWORD PTR [rsp+rax*1+0x20] ; movaps xmm0, *ptr2
   addps  xmm0,XMMWORD PTR [rsp+rax*1+0x10] ; addps xmm0, *ptr1
   movaps XMMWORD PTR [rsp+rax*1+0x30],xmm0 ; movaps *outptr, xmm0
   add    rax,0x10                          ; ptr1++
   cmp    ecx,edx
   ja     4005c0 <intrinsics_sum+0x80>
  1. Dopiero teraz możemy zabrać się do właściwego zadania czyli napisania wstawki asemblerowej.
    Najważniejsza część to oczywiści sekwencja movaps, addps, movaps.
    Niestety nie da się po prostu skopiować powyższego asm-owego listingu i owrappować
    w keyword "asm volatile ". Gcc de facto nie rozumie tego co się dzieje wewnątrz wstawki,
    więc nie wie jakich rejestrów/adresów używamy, co modyfikujemy etc. Trzeba wyrazić to explicite.
    Dodatkowo wstawka musi jakoś komunikować się z kodem w C. Stąd potrzeba użycia extended inline assembly.
 
static void assemblyx64_sum()
{
    volatile unsigned iterations = 1;

    struct Vector v1 = { 1.0f, 4.0f, 9.0f, 16.0f };
    struct Vector v2 = { 2.0f, 8.0f, 18.0f, 32.0f };
    struct Vector out = { 0.0f, 0.0f, 0.0f, 0.0f };

    __m128 *ptr1 = (__m128 *)&v1;
    __m128 *ptr2 = (__m128 *)&v2;
    float *outptr = (float *)&out;
    __m128 tmp;
    for (unsigned i = 0; i < iterations; i++)
    {
         __asm__ __volatile__(
         ".intel_syntax noprefix\n"

         "movaps %[tmp], XMMWORD PTR [%[ptr1]+%q[idx]*4]\n"
         "addps  %[tmp], XMMWORD PTR [%[ptr2]+%q[idx]*4]\n"
         "movaps XMMWORD PTR [%[outptr]+%q[idx]*4], %[tmp]\n"

         ".att_syntax prefix\n"
            : [tmp] "=x" (tmp)
            : [outptr] "r" (outptr), [ptr1] "r" (ptr1), [ptr2] "r" (ptr2),
            [idx] "r" (i)
            : "memory"
         );
         outptr += 4;
         ptr1++;
         ptr2++;
    }
    assert( EQUAL(out.x1, 3.0f) &&  EQUAL(out.x2, 12.0f)
           && EQUAL(out.x3, 27.0f) && EQUAL(out.x4, 48.0f));
}

Uwagi do assemblyx64_sum:

  • mamy 3 grupy operandów: wyjściowe (constraint "=x"), wejściowe (constraint "r" - rejestry wejściowe) oraz clobbered (constraint "memory").
  • Operandem wyjściowym jest zmienna tmp, która będzie trzymana w rejestrze lub pamięci [tmp]. Informujemy w ten sposób gcc, żeby wziął pod uwagę iż wartości, które będą tam ładowane przed wykonaniem naszej wstawki mogą być zmienione po jej wykonaniu.
  • Operandami wejściowymi są outptr, ptr1, ptr2 oraz i.
  • "memory" oznacza, że w inline assembly robimy write-y i read-y na pamięci. Zatem oprócz zmiany operandów wyjściowych wstawka wykona pewne skutki uboczne na pewnych adresach. Gcc musi wziąć to pod uwagę w czasie optymalizacji.
  • dodatkowo z niestandardowych rzeczy przed [idx] użyty został tzw. operand modifier ("q"). Bez tego modyfikatora pod [idx] podstawiony zostanie defaultowo rejestr 32-bitowy (double word) a nie 64-bitowy (q - quad word) czego byśmy nie chcieli.

Polecam zajrzeć tutaj:
Ref1: https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
Ref2: https://software.intel.com/sites/landingpage/IntrinsicsGuide/

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.