[ASM] Rejestry xmm i tablice

0

Witam.

Umieściłbym temat w dziale newbie, bo to o co mam zamiar zapytać, zapewne dla obeznanych z tematem jest głupie i banalne, no ale takiego działu już nie ma... Do rzeczy. Piszę to jako ASMową wstawkę do Delphi. Mam tablicę złożoną z bajtów i chce ją modyfikować w pętli, używając rejestrów xmm (żeby było szybciej), jednak jako zupełnie początkujący w ASM nie mogę sobie z tym poradzić. Dla zwykłych rejestrów kod wyglądałby np. tak:

lea eax, Tab //Tab to właśnie ta tablica
mov [eax], ZmiennaA
mov [eax + 1], ZmiennaB

itd.

Sęk w tym, że lea z rejestrami xmm działać nie chce i nie mam pojęcia jak wczytać do nich adres tej tablicy. Normalnie zaglądam sobie do debugera i od niego "zgapiam" jak rozwiązać pewne rzeczy, ale kompilator Delphi domyślnie rejestrow xmm nie używa.

I druga rzecz. Mógłby ktoś polecić jakieś dobre kurs źródła (po polsku lub angielsku) do nauki ASM pod x86? Chodzi mi o pisanie wstawek, więc nie muszę znać go perfekcyjnie, ale chciałbym opanować jakieś solidne podstawy.

0

lea jest do obliczania adresu, a w SSEx, AVX, AVX2 adres jest tylko jeden. Nie ma wektora adresów. Wektor adresów jest dopiero przy AVX-512 scatter i gather, ale wtedy i tak nie używasz instrukcji lea do wypełniania wektorów z adresami (przynajmniej nie bezpośrednio). lea nie jest też nikomu do niczego potrzebne w tym sensie, że można się obejść bez tego. W końcu jedyne co robi lea to proste instrukcje arytmetyczne. Jednak to lea możesz zostawić bo prawdopodobnie jest OK i wtedy zakładając że kod który podałeś jest OK:

lea eax, Tab //Tab to właśnie ta tablica
mov [eax], ZmiennaA
mov [eax + 1], ZmiennaB

w wersji pod SIMD będziesz miał np:

lea eax, Tab //Tab to właśnie ta tablica
movups [eax], WektorA
movups [eax + 16], WektorB

I druga rzecz. Mógłby ktoś polecić jakieś dobre kurs źródła (po polsku lub angielsku) do nauki ASM pod x86? Chodzi mi o pisanie wstawek, więc nie muszę znać go perfekcyjnie, ale chciałbym opanować jakieś solidne podstawy.

Jeśli klepiesz niskopoziomowy kod tylko okazjonalnie to lepiej używać tzw compiler intrinsics. Kompilator wtedy lepiej sprawdzi poprawność kodu i nie trzeba będzie w kółko debugować.

0
Wibowit napisał(a):

w wersji pod SIMD będziesz miał np:

lea eax, Tab //Tab to właśnie ta tablica
movups [eax], WektorA
movups [eax + 16], WektorB

No właśnie nic z tego, przy linicje movups [eax], WektorA wywala mi "E2116 Invalid combination of opcode and operands".

Jeśli klepiesz niskopoziomowy kod tylko okazjonalnie to lepiej używać tzw compiler intrinsics. Kompilator wtedy lepiej sprawdzi poprawność kodu i nie trzeba będzie w kółko debugować.

Chciałbym, ale z tego co mi wiadomo, Delphi natywnie nie obsługuje SIMD i trzeba sobie samemu wstawki napisać.

0

Pokaż konkretny kod, który chcesz kompilować. Przy movups oba argumenty muszą być wektorami tego samego typu.

0
Wibowit napisał(a):

Pokaż konkretny kod, który chcesz kompilować. Przy movups oba argumenty muszą być wektorami tego samego typu.

No właśnie jeszcze nie mam konkretnego kodu, tylko dłubię sobie takie przymiarki. Może opiszę co konkretnie chcę osiągnąć, to będzie łatwiej ogarnąć. Piszę sobie software'owy raster trójkątów. Najpierw wyznaczam prostokątną otoczkę trójkąta (bounding box), a następnie dzielę ją na segmenty (tiles) o wymiarach 8x8 pikseli i - po przeprowadzeniu wcześniejszych testów - wykonuję na każdym takim segmencie jedną z 3 operacji: Zostawiam w spokoju (nie robię nic), zamalowuję cały jednolitym kolorem, przeprowadzam dokładniejsze testy i zamalowuję segment piksel po pikselu. Czytałem w różnych opracowaniach, że przy tej operacji najlepiej skorzystać właśnie z SIMD, bo dzięki temu jeden taki segment można załatwić w jednym cyklu procesora. Z tego co wiem, Delphi nie posiada żadnych struktur, które by na to pozwalały i trzeba napisać własny kod w ASM, no więc staram się to ogarnąć.

U mnie to wygląda tak, że mam bufor klatki obrazu w postaci jednowymiarowej tablicy array of Integer (używam 32 bitowego formatu RGB). Chciałbym w pętli skakać do odpowiednich segmentów i wykonywać na nich wcześniej opisane operacje, możliwie jak najszybciej.

0
Crow napisał(a):

Najpierw wyznaczam prostokątną otoczkę trójkąta (bounding box), a następnie dzielę ją na segmenty (tiles) o wymiarach 8x8 pikseli i - po przeprowadzeniu wcześniejszych testów - wykonuję na każdym takim segmencie jedną z 3 operacji

Brzmi dość kiepsko. Dostęp do pamięci powinien być jak najbardziej liniowy żeby osiągnąć największą szybkość. W GPU stosuje się pewną technikę, która służy do upakowania sąsiednich pikseli do liniowej pamięci nie tylko dla relacji poziomej, ale też pionowej:
https://en.wikipedia.org/wiki/Z-order_curve#Texture_mapping

Texture mapping
Some GPUs store texture maps in Z-order to increase spatial locality of reference during texture mapped rasterization. This allows cache lines to represent rectangular tiles, increasing the probability that nearby accesses are in the cache. At a larger scale, it also decreases the probability of costly, so called, "page breaks" (i.e the cost of changing rows) in SDRAM/DDRAM. This is important because 3d rendering involves arbitrary transformations (rotations, scaling, perspective, and distortion by animated surfaces).
These formats are often referred to as swizzled textures or twidled textures. Other tiled formats may also be used.

Na CPU (podczas nakładania efektów na obrazy 2D) zwykle leci się całymi liniami i nie kombinuje z adresowaniem w taki sposób jak w GPU. Np jeśli masz obraz o wysokości 1000 pikseli to możesz go podzielić na 20 segmentów o wysokości 50 pikseli każdy, opakować każdy segment w osobne zadanie i wysłać te zadania do wykonania na puli wątków.

Jeśli chcesz wypełnić pamięć za pomocą SIMDa to:

  1. Najpierw musisz wypełnić rejestr SIMDowy pikselami
  2. Potem użyć tego rejestru do wypełnienia pamięci
0
Wibowit napisał(a):

Jeśli chcesz wypełnić pamięć za pomocą SIMDa to:

  1. Najpierw musisz wypełnić rejestr SIMDowy pikselami
  2. Potem użyć tego rejestru do wypełnienia pamięci

No i dokładnie to próbuję robić, tylko nie wiem jak ;/.

0

Gdzie masz wypełanianie rejestru SIMDowego pikselami?

0
Wibowit napisał(a):

Gdzie masz wypełanianie rejestru SIMDowego pikselami?

const
  Mask: array[0..15] of Byte = (10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 110, 120, 130, 140, 150);

var
  Tab: array[0..47] of Byte;

procedure Fill;
asm
  movups xmm0, Mask //Wypełnia rejestr zawartością maski
end;

I co dalej? Jak mam teraz przerzucić zawartość rejestru xmm0 do tablicy?

0

Jeśli movups xmm0, Mask się kompiluje to spróbuj dać:

movups xmm0, Mask
movups [Tab + 0], xmm0
movups [Tab + 16], xmm0
movups [Tab + 32], xmm0

Nie wiem jaka tam jest składnia tego asemblera. W składni Intela samo Mask powinno oznaczać adres, więc movups xmm0, Mask chyba nie powinno się kompilować. Zamiast tego powinno być movups xmm0, [Mask]. Kombinuj, ja tam nie mam Delphiego u siebie.

0
Wibowit napisał(a):

Jeśli movups xmm0, Mask się kompiluje to spróbuj dać:

movups xmm0, Mask
movups [Tab + 0], xmm0
movups [Tab + 16], xmm0
movups [Tab + 32], xmm0

Od tego sam właśnie zacząłem (bo wydawało mi się to najbardziej logiczne, zwłaszcza, że ze zwykłymi rejestrami się kompiluje), ale nic z tego, wywala mi błąd: E2107 Operand size mismatch.

Kombinuj, ja tam nie mam Delphiego u siebie.

No staram się, ale właśnie nic nie chce działać, a że w ASM jestem cienki, to w akcie desperacji stworzyłem ten wątek ;(.

0

Myślałem że chociaż ta pierwsza instrukcja ci zadziałała. Napisałeś tak jakby działała. Spróbuj:

movups xmm0, [Mask]
movups [Tab + 0], xmm0
movups [Tab + 16], xmm0
movups [Tab + 32], xmm0
0
Wibowit napisał(a):

Myślałem że chociaż ta pierwsza instrukcja ci zadziałała. Napisałeś tak jakby działała. Spróbuj:

movups xmm0, [Mask]
movups [Tab + 0], xmm0
movups [Tab + 16], xmm0
movups [Tab + 32], xmm0

Pierwsza działa i kompiluje się w obu postaciach:

movups xmm0, [Mask]
movups xmm0, Mask

Natomiast już próby zapisywania zawartości rejestru do pamięci, nie ;/.

0

W C mi takie coś zadziałało: link
Na składni asma w Delphi się nie znam, a nie ma narzędzi online do odpalania kodu w Delphi.

0

A co robi ten kod?

mov ebx, 0x41424344; //Zapisuje do ebx ten adres?. Ale czemu akurat ten, czego to adres?
movd xmm0, ebx; //Przepisuje do xmm0 zawartość ebx?
punpckldq xmm0, xmm0; //Przestawia kolejność bitów? Ale po co?
punpcklqdq xmm0, xmm0; //Jak wyżej?
movups [%0 + 0], xmm0; //Zapisuje zawartość xmm0 pod adresem, ale czemu akurat tym, na co on wskazuje?
movups [%0 + 16], xmm0; //Jak wyżej
: "=r"(a) //Nie mam pojęcia
:
: "ebx" //Nie mam pojęcia
0
mov ebx, 0x41424344; // zapisuje do ebx stałą zawierającą kody ASCII ciągu "ABCD"
movd xmm0, ebx; // kopiuje zawartość ebx do 32-najniższych bitów xmm0
punpckldq xmm0, xmm0; // duplikuje podwójne słowa, po tym 64-bity będą wypełnione stałą z początku
punpcklqdq xmm0, xmm0; // duplikuje poczwórne słowa, po tym całe 128-bitów będzie wypełnione stałą z początku
movups [%0 + 0], xmm0; // Zapisuje zawartość xmm0 pod adresem, a %0 to specjalna składnia GCC
movups [%0 + 16], xmm0; //Jak wyżej
// zarówno %0 jak i poniższe rzeczy są opisane na https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s5
: "=r"(a)
:
: "ebx"
0

"Odkopuję", bo udało mi się to rozgryźć i chciałem zapytać o coś jeszcze.

Miałem oryginalny kod:

procedure TDisplay.Fill(const AColor: Cardinal);
var
  I: Cardinal;
  P: ^Cardinal;
begin
  P := @Buffer.Color[0];
  for I := 0 to Buffer.Size - 1 do
    begin
      P^ := AColor;
      Inc(P, 1);
    end;
end;

I przerobiłem go na:

procedure TDisplay.FillASM(const AColor: Cardinal);
var
  Mask: array[0..3] of Integer;
asm
  lea ebx, Mask;
  mov edx, AColor;
  mov [ebx], edx;
  mov [ebx + 4], edx;
  mov [ebx + 8], edx;
  mov [ebx + 12], edx;
  movups xmm0, [ebx];
  mov ebx, [eax].Buffer.Color[0]; //Buffer.Color to jednowymiarowa tablica dynamiczna wypełniona integerami
  mov ecx, [eax].Buffer.Size; //Buffer.Size to jego szerokość pomnożona przez jego długość, np. 1920 x 1080
  shr ecx, 2;
  mov eax, 0;
@Loop:
  movups [ebx + eax], xmm0;
  add eax, 16;
  dec ecx;
  jnz @Loop;
end;

Różnica szybkości jest KOLOSALNA! pętla z 1000 powtórzeń w przypadku pierwszego kodu dawała 400-410 milisekund. Kod w ASM zamyka się w granicach... 27-28 ms! Próbowałem też wersji kodu, w której wykorzystałem więcej niż 1 rejestr, tzn coś takiego:

procedure TDisplay.FillASM(const AColor: Cardinal);
var
  Mask: array[0..3] of Integer;
asm
  lea ebx, Mask;
  mov edx, AColor;
  mov [ebx], edx;
  mov [ebx + 4], edx;
  mov [ebx + 8], edx;
  mov [ebx + 12], edx;
  movups xmm0, [ebx];
  movups xmm1, xmm0;
  movups xmm2, xmm0;
  movups xmm3, xmm0;
  mov ebx, [eax].Buffer.Color[0];
  mov ecx, [eax].Buffer.Size;
  shr ecx, 4;
  mov eax, 0;
@Loop:
  movups [ebx + eax], xmm0;
  movups [ebx + eax + 16], xmm1;
  movups [ebx + eax + 32], xmm2;
  movups [ebx + eax + 48], xmm3;
  add eax, 64;
  dec ecx;
  jnz @Loop;
end;

Jednak nie miało to żadnego wpływu na szybkość działania. Jako że nie jestem zbyt biegły w ASM, nie jestem pewien z czym to jest związane. Czy chodzi o to, że CPU w jednym cyklu może dokonać tylko jednej operacji, polegającej na zapisie wartości w rejestrze do pamięci?
Drugie pytanie jest bardziej prozaiczne. Czy mój kody jest dobrze napisany? Wszystko optymalizuję tak jak trzeba? Mam wątpliwości zwłaszcza z tym zapisywaniem maski do rejestrów xmm, ale nie wiem, czy da się to zrobić lepiej. I czy maska w takiej formie jest konieczna? Jak samodzielnie alokować pamięć w ASM?

0

Jednak nie miało to żadnego wpływu na szybkość działania. Jako że nie jestem zbyt biegły w ASM, nie jestem pewien z czym to jest związane. Czy chodzi o to, że CPU w jednym cyklu może dokonać tylko jednej operacji, polegającej na zapisie wartości w rejestrze do pamięci?

Bardziej prawdopodobne, że przepustowość kontrolera pamięci jest wąskim gardłem. Spróbuj zamiast 16 bajtów naraz wypełniać 4-ema.

Mam wątpliwości zwłaszcza z tym zapisywaniem maski do rejestrów xmm, ale nie wiem, czy da się to zrobić lepiej. I czy maska w takiej formie jest konieczna?

Nie trzeba w ten sposób. Można się obejść bez dodatkowej pamięci (obojętne czy na stercie czy stosie). Zamiast kopiowania danych między pamięcią, a rejestrami można zastosować rozwiązanie z punpck...

Jak samodzielnie alokować pamięć w ASM?

Jeśli chcesz coś zaalokować na stercie to coś a'la malloc (tzn ten sam mechanizm co w języku goszczącym), a jeśli na stosie to arytmetyka na wskaźniku stosu (języki wyższego poziomu też to robią, ale to chowają przed programistą).

Czy mój kody jest dobrze napisany?

A nie wiem, nie ma jak sprawdzić, bo Delphi nie ma na żadnej testerce kodu źródłowego.

0
Wibowit napisał(a):

Nie trzeba w ten sposób. Można się obejść bez dodatkowej pamięci (obojętne czy na stercie czy stosie). Zamiast kopiowania danych między pamięcią, a rejestrami można zastosować rozwiązanie z punpck...

Mógłbym prosić o coś więcej? :). Jak tego użyć? Chciałem sprawdzić jak pamięć alokuje kompilator Delphi ale on odwołuje się do jakiegoś makra:

mov eax X //gdzie X, tam ilość alokowanych bajtów
call @GetMem
mov [ebp-$08], eax

Nie wiem natomiast co robi wewnątrz tego makra i ja go wywołać w swoim kodzie nie mogę, bo kompilator krzyczy, że nie ma takiej etykiety (label) jak @GetMem.

Jeśli chcesz coś zaalokować na stercie to coś a'la malloc (tzn ten sam mechanizm co w języku goszczącym), a jeśli na stosie to arytmetyka na wskaźniku stosu (języki wyższego poziomu też to robią, ale to chowają przed programistą).

Teraz mam tak:

asm
  push AColor;
  push AColor;
  push AColor;
  push AColor;

  movups xmm0, [esp];
  mov ebx, [eax].Buffer.Color[0];
  mov ecx, [eax].Buffer.Size;

  add esp, 16;
  shr ecx, 2;
  mov edx, 0;
@Loop:
  movups [ebx + edx], xmm0;
  add edx, 16;
  dec ecx;
  jnz @Loop;
end;

Dobrze to robię? Jest jakiś szybszy sposób na umieszczenie 16 bajtów na stosie, niż poprzez czterokrotny push (każdy zawiera 4-bajtowy kolor)?

A nie wiem, nie ma jak sprawdzić, bo Delphi nie ma na żadnej testerce kodu źródłowego.

Ja pytam o samą składnię i sposoby realizowania poszczególnych zadań (tj. czy np. nie wykonuję zbędnych czynności, które spowalniają działanie). Bo tak poza tym, to kod się kompiluje i działa.

0

Nie wiem natomiast co robi wewnątrz tego makra i ja go wywołać w swoim kodzie nie mogę, bo kompilator krzyczy, że nie ma takiej etykiety (label) jak @GetMem

Spróbuj zamiast @GetMem dać samo GetMem. Czy to GetMem działa poza kodem asmowym?

Dobrze to robię? Jest jakiś szybszy sposób na umieszczenie 16 bajtów na stosie, niż poprzez czterokrotny push (każdy zawiera 4-bajtowy kolor)?

Już napisałem, żeby użyć punpck... i nie bawić się w kopiowanie pamięci poza rejestrami. Już podałem link do kodu w C++ więc można go przenieść do Delphi. Np zamiast

  lea ebx, Mask;
  mov edx, AColor;
  mov [ebx], edx;
  mov [ebx + 4], edx;
  mov [ebx + 8], edx;
  mov [ebx + 12], edx;
  movups xmm0, [ebx];

zrobić:

  mov edx, AColor;
  movd xmm0, edx;
  punpckldq xmm0, xmm0;
  punpcklqdq xmm0, xmm0;

a może nawet zadziała:

  movd xmm0, AColor;
  punpckldq xmm0, xmm0;
  punpcklqdq xmm0, xmm0;

Jeśli chcesz się dowiedzieć co robią te instrukcje to zobacz np na to: http://qcd.phys.cmu.edu/QCDcluster/intel/vtune/reference/vc265.htm

Ja pytam o samą składnię i sposoby realizowania poszczególnych zadań (tj. czy np. nie wykonuję zbędnych czynności, które spowalniają działanie). Bo tak poza tym, to kod się kompiluje i działa.

Spowalnianie to jedno, ale ważniejsza jest poprawność. Dla przykładu trzeba poszukać które rejestry można bezkarnie nadpisywać, a które trzeba przywracać przed powrotem z kodu asmowego lub przed wywołaniem procedur.

 mov ebx, [eax].Buffer.Color[0]; //Buffer.Color to jednowymiarowa tablica dynamiczna wypełniona integerami
...
 movups [ebx + eax], xmm0;

Co tu się dzieje? Do ebx wstawiasz kolor, a potem używasz tego koloru jako adresu bazowego? Dziwnie to wygląda.

Testowałeś w ogóle poprawność swojego kodu? W sensie sprawdzenia czy nadpisuje dokładnie tyle pamięci ile trzeba, ani mniej ani więcej.

1 użytkowników online, w tym zalogowanych: 0, gości: 1