2012-04-16 17 views
6

Uczę się korzystać z możliwości SIMD, ponownie pisząc moją osobistą bibliotekę przetwarzania obrazów, używając wektorów wewnętrznych. Jedną z podstawowych funkcji jest proste "array +=", czyliDodawanie tablicy SIMD dla dowolnych długości tablic

void arrayAdd(unsigned char* A, unsigned char* B, size_t n) { 
    for(size_t i=0; i < n; i++) { B[i] += A[i] }; 
} 

Dla dowolnych długościach tablicy, oczywistym kod SIMD (zakładając dopasowane przez 16 lat) jest coś takiego jak:

size_t i = 0; 
__m128i xmm0, xmm1; 
n16 = n - (n % 16); 
for (; i < n16; i+=16) { 
    xmm0 = _mm_load_si128((__m128i*) (A + i)); 
    xmm1 = _mm_load_si128((__m128i*) (B + i)); 
    xmm1 = _mm_add_epi8(xmm0, xmm1); 
    _mm_store_si128((__m128i*) (B + i), xmm1); 
} 
for (; i < n; i++) { B[i] += A[i]; } 

Ale czy to możliwe do all dodatki za pomocą instrukcji SIMD? Pomyślałem o wypróbowaniu tego:

__m128i mask = (0x100<<8*(n - n16))-1; 
_mm_maskmoveu_si128(xmm1, mask, (__m128i*) (B + i)); 

dla dodatkowych elementów, ale czy spowoduje to niezdefiniowane zachowanie? mask powinien zagwarantować, że dostęp nie zostanie faktycznie przekroczony poza granicami tablicy (chyba). Alternatywą jest najpierw wykonanie dodatkowych elementów, ale tablica musi zostać wyrównana przez n-n16, co nie wydaje się właściwe.

Czy istnieje inny, bardziej optymalny wzór takich wektoryzowanych pętli?

+0

można upewnić się, że w kodzie długości tablicy są zawsze wielokrotnością 16 bajtów (choć być może mniej elementy są rzeczywiście używane), więc ten epilog nigdy nie wyjdzie. Ale epilog naprawdę nie jest ważny pod względem szybkości. – Walter

Odpowiedz

4

Jedną z opcji jest umieszczenie tablicy w wielokrotności 16 bajtów. Następnie możesz zrobić 128-bitowe ładowanie/dodawanie/zapisywanie i po prostu ignorować wyniki po punkcie, na którym Ci zależy.

Dla dużych tablic, chociaż narzut bajta na bajt "epilog" będzie bardzo mały. Rozwijanie pętli może zwiększyć wydajność, coś w stylu:

for (; i < n32; i+=32) { 
    xmm0 = _mm_load_si128((__m128i*) (A + i)); 
    xmm1 = _mm_load_si128((__m128i*) (B + i)); 
    xmm2 = _mm_load_si128((__m128i*) (A + i + 16)); 
    xmm3 = _mm_load_si128((__m128i*) (B + i + 16)); 
    xmm1 = _mm_add_epi8(xmm0, xmm1); 
    xmm3 = _mm_add_epi8(xmm2, xmm3); 
    _mm_store_si128((__m128i*) (B + i), xmm1); 
    _mm_store_si128((__m128i*) (B + i + 16), xmm3); 
} 
// Do another 128 bit load/add/store here if required 

Trudno powiedzieć, nie robiąc profilowania.

Można również wykonać niewymagane ładowanie/przechowywanie na końcu (zakładając, że masz więcej niż 16 bajtów), chociaż prawdopodobnie nie spowoduje to dużej różnicy. Na przykład. jeśli masz 20 bajtów wykonać jedną ładowania/zapisu do przesunięcia 0 i kolejny niezestrojone obciążenia/dodać/sklep (_mm_storeu_si128, __mm_loadu_si128), aby zrównoważyć 4.

Można użyć _mm_maskmoveu_si128 ale trzeba dostać maskę do rejestru XMM , a twój przykładowy kod nie zadziała. Prawdopodobnie chcesz ustawić rejestr masek dla wszystkich FF, a następnie użyć przesunięcia, aby wyrównać. Pod koniec dnia prawdopodobnie będzie wolniejszy niż niezaliczone ładowanie/dodawanie/zapisywanie.

To byłoby coś jak:

mask = _mm_cmpeq_epi8(mask, mask); // Set to all FF's 
mask = _mm_srli_si128(mask, 16-(n%16)); // Align mask 
_mm_maskmoveu_si128(xmm, mask, A + i); 
+0

W praktyce umieściłbym maski w tabeli odnośników. Czy myślisz, że byłaby wolniejsza od pętli "epilog"? –

+0

@reve_etrange: Prawdopodobnie nie wolniej, ale trudno o tym wiedzieć bez pomiaru dwóch rozwiązań. Spróbuj. –

+0

Dam ci szansę. Ale czy jest to legalny dostęp do pamięci? Ponieważ * pewna * wartość 'maski' może spowodować naruszenie granic tablicy. –