Wykorzystanie instrukcji wektorowych (MMX) w asemblerze

0

Cześć, jestem w trakcie pisania programu tworzącego negatyw bitmapy. Funkcja, która wykonuje ten negatyw pobiera jednowymiarową tablicę bajtów, rozmiary tablicy oraz zmienną do której zapisuje ilość cykli procesora. Wygląda mniej więcej tak:

Negatyw24 PROC stdcall uses eax ebx ecx edx, tab :dword, amount :dword, cycles :dword

	RDTSC
	mov ECX, cycles				;pobranie adresu tablicy
	mov [ECX], EDX				;zapis starszej polowki licznika
	mov [ECX+4], EAX			;zapis mlodszej polowki licznika
	
	mov EAX, tab	;kopiuj adres 1 komorki
	add EAX, amount	;dodaj ilosc komorek
	sub EAX, 1		;przjedz do ost komorki
loop:
	mov BL, [EAX]	;pobierz komorke do rej
	mov CL, 255		;laduj FF do CL
	sub CL, BL		;neguj bajt w BL
	mov [EAX], CL	;zapisz zaneg bajt do pao
	cmp EAX, tab	;sprawdz koniec tablicy
	je end
	sub EAX, 1		;przesun sie w tablicy o 1 komorke do tylu
	jmp loop
end:	
	RDTSC						;dokonaj drugiego odczytu licznika cykli
	mov ECX, cycles	
	
	mov EBX, [ECX]				;odczytaj z pamieci poprzednie pomiary
	mov EBX, [ECX+4]
	
	sub EAX, [ECX+4]		;oblicz roznice
	sbb EDX, [ECX]		
	mov [ECX], EDX			;i zapisz rezultat do kolejnych dwoch pol tablicy
	mov [ECX+4], EAX

    ret 

Negatyw24 ENDP

Aktualnie mój kod napisany jest bez wykorzystania instrukcji procesora MMX. Czy byłby ktoś w stanie tak przerobić kod odpowiedzialny za samo wykonanie negatywu:

mov EAX, tab	;kopiuj adres 1 komorki
	add EAX, amount	;dodaj ilosc komorek
	sub EAX, 1		;przjedz do ost komorki
loop:
	mov BL, [EAX]	;pobierz komorke do rej
	mov CL, 255		;laduj FF do CL
	sub CL, BL		;neguj bajt w BL
	mov [EAX], CL	;zapisz zaneg bajt do pao
	cmp EAX, tab	;sprawdz koniec tablicy
	je end
	sub EAX, 1		;przesun sie w tablicy o 1 komorke do tylu
	jmp loop
end:	

aby wykorzystać te instrukcje MMX? Może to być zrobione sztucznie, czyli zamiast int stosować float, cokolwiek. Wszelkie moje próby kończyły się niepowodzeniem, stąd prośba do Was. Niestety mam takie wymagania projektowe i muszę to tak zrobić.

0

To pokaż te próby.

Jest instrukcja PSUBB do odejmowania wektorów złożonych z bajtów, ale do celu negatywu wystarczy PXOR z samymi jedynkami (czyli negowanie bitów po prostu).

Wykorzystując instrukcje wektorowe musisz pamiętać o obsłużeniu przypadku, gdy rozmiar danych wejściowych nie jest wielokrotnością rozmiaru sprzętowego wektora. W takim przypadku robisz wektorowo tyle ile się da, a resztkę na końcu obsługujesz kodem szeregowym.

0

Nie mam tych prób bo je usunąłem i przywróciłem stary kod skoro nie działały. A mógłbym prosić o przykład z użyciem PXOR?

0

Może zrobię to inaczej.

Masz kod typu:

// lecimy sobie po jednym bajcie
for (int i = 0; i < size; i++) {
  tab[i] = 255 - tab[i];
}

Najpierw musisz go przerobić na kod typu:

// lecimy sobie po 8 bajtowych kawałkach
for (int i = 0; i < size / 8; i++) {
  for (int j = i * 8; j < (i + 1) * 8; i++) {
    tab[j] = 255 - tab[j];
  }
}
// lecimy po końcówce która nie utworzyła 8 bajtowego kawałka
for (int i = size / 8 * 8; i < size; i++) {
  tab[j] = 255 - tab[j];
}

Teraz można zastąpić wewnętrznego fora instrukcjami MMX:

vec8b mask = same jedynki;
for (int i = 0; i < size / 8; i++) {
  // załadowanie wektora danymi od pozycji i * 8 do i * 8 + 7 włącznie
  vec8b input = tab[i * 8 : i * 8 + 7];
  vec8b result = PXOR(input, mask);
  tab[i * 8 : i * 8 + 7] = result
}
// lecimy po końcówce która nie utworzyła 8 bajtowego kawałka
for (int i = size / 8 * 8; i < size; i++) {
  tab[j] = 255 - tab[j];
}
0

Dobra, zrozumiałem już ideę działania tego, ale niestety napisanie tego w asemblerze trochę przerasta moje umiejętności. Mógłbym prosić o przykład w asm?

2

Wymodziłem na szybko coś takiego:

#include <mmintrin.h>

void negatyw(unsigned char *tab, size_t len)
{
	size_t len8 = len / 8;

	_mm_empty();
	__m64 *mtab = (__m64*)tab;
	__m64 ones = _mm_set_pi32(0xFFFFFFFF, 0xFFFFFFFF);

	// główna część
	for (size_t i = 0; i < len8; i++)
	{
		mtab[i] = _mm_xor_si64(mtab[i], ones);
	}
	_mm_empty();

	// końcówka
	for (size_t i = len8 * 8; i < len; i++)
	{
		tab[i] = ~tab[i];
	}
}

int main()
{
	unsigned char tablica[999];
	negatyw(tablica, sizeof(tablica));
	return 0;
}

Nie sprawdzałem poprawności działania, kod się kompiluje i nie zawiesza. Wygenerowany kod w asemblerze wygląda tak:

00A6101A  emms  
00A6101C  mov         dword ptr [esp+4],0FFFFFFFFh  
00A61024  xor         eax,eax  
00A61026  mov         dword ptr [esp],0FFFFFFFFh  
00A6102D  movq        mm2,mmword ptr [esp]  
00A61031  movq        mm1,mmword ptr [esp+eax*8+8]  
00A61036  pxor        mm1,mm2  
00A61039  movq        mmword ptr [esp+eax*8+8],mm1  
00A6103E  inc         eax  
00A6103F  cmp         eax,7Ch  
00A61042  jb          main+31h (0A61031h)  
00A61044  emms  
00A61046  mov         eax,3E0h  
00A6104B  jmp         main+50h (0A61050h)  
00A6104D  lea         ecx,[ecx]  
00A61050  not         byte ptr [esp+eax+8]  
00A61054  inc         eax  
00A61055  cmp         eax,3E7h  
00A6105A  jb          main+50h (0A61050h)  

Możesz sobie go postudiować.

0

Dziękuję, spróbuję teraz sobie z tym poradzić, powinno być łatwiej.

0

A jakbym chciał wykonać to samo, tyle że na instrukcjach SSE to będzie wyglądać analogicznie?

1

pod SSE:

  • nie używa się instrukcji emms
  • rejestry nazywają się xmm1 a nie mm1
  • rejestry mają 128 bitów (16 bajtów) a nie 64 bity (8 bajtów)
  • dane powinny być wyrównane do granicy 16 bajtów (pod MMX - 8 bajtów)
  • większość instrukcji nazywa się i działa tak samo

wyrównanie pod visualem można osiągnąć pisząc __declspec(align(16)) unsigned char tab[N];
jeśli wyrównanie jest nieosiągalne, to trzeba dodać na początku pętlę ze zwykłym działaniem podobną do tej końcowej.

void negatyw(unsigned char *tab, size_t len)
{
    size_t lenm = len / 16;
 
    __m128i *mtab = (__m128i*)tab;
    __m128i ones = _mm_set1_epi8((char)0xFF);
 
    // główna część
    for (size_t i = 0; i < lenm; i++)
    {
        mtab[i] = _mm_xor_si128(mtab[i], ones);
    }
 
    // końcówka
    for (size_t i = lenm * 16; i < len; i++)
    {
        tab[i] = ~tab[i];
    }
}

Jakoś tak. Nie sprawdzane, więc kodu w asm też nie podam. Z tego co pamiętam, zamiast movq (move quadword) trzeba napisać movdqa (move double quadword aligned).

Generalnie polecam używanie funkcji "intrinsic" w C zamiast babranie się bezpośrednio w asemblerze. Jak widzisz po przykładzie wyżej, kod wygenerowany przez kompilator (przy maksymalnej optymalizacji i w trybie release) jest całkiem czysty.

0

Jakbym nie musiał to w ogóle bym tego w asemblerze nie robił, ale że taki projekt to cóż poradzić :) dzięki Ci bardzo, będę walczył!

0

Cześć, starałem się coś zrobić z tymi instrukcjami wektorowymi (SSE konkretnie) ale cały czas dostaje albo heap corruption albo "program przestał działać"... :( Pomożecie jeszcze? Aktualny kod asm:

;-------------------------------------------------------------------------
.686 
.mmx
.xmm
.MODEL flat, stdcall


OPTION CASEMAP:NONE

INCLUDE    include\windows.inc
INCLUDE    include\user32.inc
INCLUDE    include\kernel32.inc 
  
.CODE

DllEntry PROC hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD

    mov eax, TRUE  
    ret

DllEntry ENDP

;-------------------------------------------------------------------------

Negatyw24 PROC stdcall uses eax ebx ecx edx edi esi, tab :dword, amount :dword
	
	mov EAX, tab	;adres początku tablicy
	add EAX, amount ;dodaj ilosc komorek
	sub EAX, 1		;przejdz do ostatniej komorki

	pcmpeqd xmm0, xmm0
	
	; -----------------------
	; Petla negowania z SSE -
	; -----------------------

	petlaSSE:
		cmp EAX, tab	;jesli koniec
		je koniecSSE	;to koniec instrukcji SSE

		movaps xmm1, [EAX]
		movaps xmm1, [EAX-1]
		movaps xmm1, [EAX-2]
		movaps xmm1, [EAX-3]
		movaps xmm1, [EAX-4]
		movaps xmm1, [EAX-5]
		movaps xmm1, [EAX-6]

		pxor	xmm1, xmm0					; zanegowanie wartości rejestrów
		pxor	xmm2, xmm0
		pxor	xmm3, xmm0
		pxor	xmm4, xmm0
		pxor	xmm5, xmm0
		pxor	xmm6, xmm0
		pxor	xmm7, xmm0

		movaps [EAX], xmm1
		movaps [EAX-1], xmm2
		movaps [EAX-2], xmm3
		movaps [EAX-3], xmm4
		movaps [EAX-4], xmm5
		movaps [EAX-5], xmm6
		movaps [EAX-6], xmm7

		sub EAX, 7	;zmniejszenie komorek o 7

		jmp petlaSSE

	koniecSSE:	

    ret 

Negatyw24 ENDP

;-------------------------------------------------------------------------

END DllEntry
1
        movaps xmm1, [EAX]
        movaps xmm1, [EAX-1]
        movaps xmm1, [EAX-2]
        movaps xmm1, [EAX-3]
        movaps xmm1, [EAX-4]
        movaps xmm1, [EAX-5]
        movaps xmm1, [EAX-6]

na pewno? po co do tyłu, dlaczego indeksujesz po jednym bajcie, i do jednego rejestru? ;-)

poza tym mówiłem o instrukcji movdqa, skąd nagle movaps?

Wszystko robię za pomocą Visual Studio 2013 Ultimate więc masm,

a musi być ten asm? Visual ma gotowe funkcje do operowania na MMX i SSE, co zresztą pokazałem.

0

Co do indeksowania po 1 bajcie to akurat tutaj tak testowałem, robiłem -16, -32 itd. też nie działało. W dół dlatego, że lecę od końca tablicy w dół, ale w sumie to jest obojętne. Co do jednego rejestru to faktycznie... ślepy niczym kret jestem przepraszam :(
Movaps znalazłem w internecie, zmienię na movdqa :)
ASM niestety musi być bo taki mam projekt na studiach i muszę tak to zrobić niestety.

1

Jak będziesz indeksował po jednym bajcie to dostaniesz banialuki. Masz skalować o rozmiar rejestru. I pamiętaj też o tym:

When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) will be generated.

Czyli nie [eax - 5] tylko [eax - 5 * 16]. Nie sub eax, 7 tylko sub eax, 7 * 16.

0

Zaczynaj od początku tablicy nie od końca, bo tak łatwiej jest dopilnować wyrównania. No i jeśli rozmiar tablicy nie jest podzielny przez ilość bajtów obrabianych w jednym przebiegu pętli, to końcówkę trzeba przerobić klasycznie (czyli bez SSE).

0

Jeśli tablica nie jest wyrównana, to początek też trzeba zrobić bez SSE.

0

Zmieniłem to tak i starałem się dodać to wyrównanie na początku ale coś średnio mi wyszło raczej.
A oto kod:

Negatyw24 PROC stdcall uses eax ebx ecx edx edi esi, tab :dword, amount :dword

	mov EAX, tab	;adres początku tablicy
	add EAX, amount ;dodaj ilosc komorek
	sub EAX, 1		;przejdz do ostatniej komorki

	pcmpeqd xmm0, xmm0

	unaligned:
		not byte ptr [eax]
		add eax, 1
		sub ecx, 1

		test al, 15
		jnz unaligned
	
	; -----------------------
	; Petla negowania z SSE -
	; -----------------------

	petlaSSE:
		cmp EAX, tab	;jesli zostalo mniej niz 112 bajtow
		jb koniecSSE	;to koniec instrukcji SSE

		movdqa xmm1, [EAX]
		movdqa xmm2, [EAX-16]
		movdqa xmm3, [EAX-32]
		movdqa xmm4, [EAX-48]
		movdqa xmm5, [EAX-64]
		movdqa xmm6, [EAX-80]
		movdqa xmm7, [EAX-96]

		pxor	xmm1, xmm0					; zanegowanie wartości rejestrów
		pxor	xmm2, xmm0
		pxor	xmm3, xmm0
		pxor	xmm4, xmm0
		pxor	xmm5, xmm0
		pxor	xmm6, xmm0
		pxor	xmm7, xmm0

		movdqa [EAX], xmm1
		movdqa [EAX-16], xmm2
		movdqa [EAX-32], xmm3
		movdqa [EAX-48], xmm4
		movdqa [EAX-64], xmm5
		movdqa [EAX-80], xmm6
		movdqa [EAX-96], xmm7

		sub EAX, 112	

		jmp petlaSSE

	koniecSSE:	

        ret 

Negatyw24 ENDP

Teraz dostaje taki oto komunikat:
user image

0

Jeśli np ostatni bajt tablicy jest pod adresem równym 1 modulo 16 to wtedy pierwsza pętla (o etykiecie unaligned) cofnie się o jeden bajt, a następna (petlaSSE) podmieni bajty za końcem tablicy. Ponadto zamiast cmp EAX, tab powinno być coś w stylu cmp EAX, tabMinus112, a następnie powinna być pętla do przetwarzania końcówki.

0

@Wibowit Średnio rozumiem twoje pierwsze zdanie i też nie wiem jak by temu zaradzić. Co do tego porównania to masz rację, zmieniłem to i teraz wygląda tak. Jednakże program ogólnie dalej się crashuje i nie wiem jak to zmienić żeby było dobrze.

	mov EAX, tab	;adres początku tablicy
	mov EBX, tab
	add EBX, amount ;uzyskaj ostatnia komorke
	sub EBX, 112	;zaladowanie maksymalnej liczby do SSE

	pcmpeqd xmm0, xmm0

	unaligned:
		not byte ptr [eax]
		add eax, 1
		;sub ecx, 1

		test al, 15
		jnz unaligned
	
	; -----------------------
	; Petla negowania z SSE -
	; -----------------------

	petlaSSE:
		cmp EAX, EBX	;jesli zostalo mniej niz 112 bajtow
		jnb koniecSSE	;to koniec instrukcji SSE
0

Zmieniłeś jb na jnb i niby ma działać tak samo?

W pierwszym zdaniu chodziło mi o to, że o ile w pierwszej pętli cofasz się do granicy 16 bajtów, to jeżeli tablica nie kończy się na granicy 16 bajtów to w drugiej pętli na samym początku dobierasz się do bajtów spoza tablicy. movdqa xmm1, [EAX] ładuje bajty od eax do eax+15 włącznie i powinieneś widzieć od razu, że to może oznaczać dobieranie się do bajtów spoza tablicy.

0

Zmieniłem to, że do rejestru EBX rozmiar tablicy i odejmuje od niego 112 czyli sumaryczny rozmiar rejestrów MMX, natomiast w EAX zostaje początek tablicy. No i teraz tutaj

        cmp EAX, EBX    ;jesli zostalo mniej niz 112 bajtow
        jnb koniecSSE    ;to koniec instrukcji SSE

wykona skok, jeśli nie mniejszy, czyli jeśli przekroczy możliwy zakres wykonania dla MMX, czyli resztę trzeba ręcznie zanegować. Tak to rozumiem.

A co do drugiego to rozumiem i to faktycznie może być przyczyną tych błędów, ale jak temu zaradzić? Bo jeśli np cofnie się 5 razy to później dobiorę się do 5 miejsc spoza tablicy...

2

Podam algo w pseudo C. Instrukcja mem(a) dobiera się do bajtu po adresem a.

int a = <adres tab>;
int n = <rozmiar>;
int i = a;
while (i < min(a + n, ((a - 1) | 15) + 1)) {
  // tutaj lecimy skalarnie, bez użycia SSE
  mem(i) ^= -1;
  i++;
}
while (i <= a + n - 7 * 16) {
  // te instrukcje oczywiście trzeba zamienić na wektorowe, 7 wektorów SSE
  mem(i) ^= -1;
  ...
  mem(i + 7 * 16 - 1) ^= -1;
  i += 7 * 16;
}
while (i < a + n) {
  // tutaj lecimy skalarnie, bez użycia SSE
  mem(i) ^= -1;
  i++;
}
0
while (i < min(a + n, ((a - 1) | 15) + 1)) {

Kompletnie tego nie rozumiem... :( szukam minimum czy jak?
Czy mógłbym Cię prosić o napisanie tego kodu w asemblerze?

Także nie rozumiem jednej rzecy... skoro w tym moim kodzie jeśli nie jest wyrównany do 16 to dodaje 1 do EAX, to czy nie powinno po prostu pominąć kilka początkowych bajtów i wykonać się dalej? W sensie ta negacja w rejestrach MMX?

1

a + n to adres końca tablicy, a ((a - 1) | 15) + 1 to pierwszy adres podzielny przez 16 nie wcześniejszy niż a. min jest po to, by nie wyjechać poza tablicę.

Także nie rozumiem jednej rzecy... skoro w tym moim kodzie jeśli nie jest wyrównany do 16 to dodaje 1 do EAX, to czy nie powinno po prostu pominąć kilka początkowych bajtów i wykonać się dalej? W sensie ta negacja w rejestrach MMX?

Nie wiem do czego pijesz, ale jeśli chodzi o sens mojego kodu to:

  • najpierw wyobraź sobie tablicę idealnie wyrównaną do granicy 16 bajtów, zarówno na początku jak i na końcu,
  • taką tablicę można ogarnąć SSE bez problemu,
  • jednak po przeniesieniu początku o kilka bajtów wstecz, a końca kilka bajtów wprzód już nie będzie można tego ogarnąć SSE,
  • trzeba więc zrobić trzy pętle - jedna do ogarnięcia niewyrównanego początku, jedna dla szybkiego przechodzenia środka za pomocą SSE i jedna do ogarnięcia niewyrównanej końcówki.
    Mój kod to wszystko robi.
0

Ok już rozumiem. Ale wychodzi na to, że nie muszę robić tego wyrównania na początku bo on i tak zawsze zaczyna od początku tej tablicy. Udało mi się to zrobić, jedyne co mi pozostało to zrobienie bez SSE końcówki tablicy, tej która jest za mała na rejestry XMM, ale to myślę, że jutro zrobię :)

Napiszę czy udało mi się to dokończyć, myślę, że już pójdzie z górki. Bardzo wam dziękuję za pomoc! :) Jakby coś to będę się jeszcze pytał :)

0

Ale sama tablica nie musi się zaczynać od adresu podzielnego przez 16, a o to właśnie chodzi. Procesora nie obchodzi co czym jest (w sensie czy tablicą, wskaźnikiem na element, itd), on po prostu oblicza adres w pamięci i ten adres musi być podzielny przez 16 jeśli używasz SSE.

0

Ale wychodzi na to, że nie muszę robić tego wyrównania na początku bo on i tak zawsze zaczyna od początku tej tablicy.

Początek tablicy też musi być wyrównany a domyślnie nie jest to zagwarantowane.

Można wymusić prawidłowe wyrównanie tablicy

__declspec(align(16)) unsigned char tablica[9999];

Wtedy nie jest potrzebna początkowa pętla i kod się uprości. Ale nie zawsze jest to wykonalne.

0

Dobrze, to w takim razie będę jutro testował mój program i dorobię wyrównanie. Dam znać o efektach.

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