2013-06-13 15 views
8

Mam dwie tablice i chcę skopiować jedną tablicę do drugiej z pewnym krokiem. Na przykład, mamKopiowanie danych z osiami w C++

A A A A A A A A ... 

B B B B B B B B ... 

i chcę skopiować co trzy elementy B do A uzyskanie

B A A B A A B A ... 

ze stanowiska „Is there a standard, strided version of memcpy?”, wydaje się, że nie ma takiej możliwości w C.

Jednakże, w niektórych przypadkach, memcpy jest szybszy niż kopia oparta na pętli for.

Moje pytanie brzmi; Czy istnieje sposób wydajnego wykonywania strided memory copy w C++ co najmniej jako standardowej pętli for?

Dziękuję bardzo.

EDIT - Wyjaśnienie PROBLEMU

Aby uczynić problem jaśniej, oznaczmy dwie tablice pod ręką przez a i b. Mam funkcji, która wykonuje niepowtarzalny następuje for pętla

for (int i=0; i<NumElements, i++) 
    a_[i] = b_[i]; 

gdzie zarówno [] „s są przeciążone operatory (używam techniki szablony wyrażenie), tak aby można je właściwie znaczy, na przykład

a[3*i]=b[i]; 
+0

Standardowa pętla wykonuje co najmniej tak szybko, jak standard dla pętli ... Poza sarkazmem, zależy to od używanej struktury przechowywania danych. W przypadku tablic nie sądzę, że można zrobić coś lepszego niż pętlę for, zwiększaną o twój moduł. – ChrisCM

+0

'memcpy' jest czasami szybszy niż pętla' for', ponieważ optymalizuje go, ponieważ pamięć, na której działa, jest ciągła. Tych optymalizacji nie można tutaj wprowadzić. –

+0

@dauphic Ale dlaczego CUDA ma 'cudaMemcpy2D', który kopiuje ze skokiem? – JackOLantern

Odpowiedz

6
Is there any way to efficiently perform strided memory copy in C++ performing at least as a standard for loop? 

Edit 2: nie ma funkcja strided kopiowania w C++ bibliotek.

Ponieważ kopiowanie z ukosowaniem nie jest tak popularne, jak kopiowanie pamięci, producenci układów scalonych i projekty językowe mają wyspecjalizowane wsparcie dla kopiowania skośnego.

Przyjmując standardową pętlę for, możesz uzyskać pewną wydajność za pomocą Loop Unrolling. Niektóre kompilatory mają opcje rozwijania pętli; to nie jest "standardowa" opcja.

Biorąc pod uwagę standardowyfor pętla:

#define RESULT_SIZE 72 
#define SIZE_A 48 
#define SIZE_B 24 

unsigned int A[SIZE_A]; 
unsigned int B[SIZE_B]; 
unsigned int result[RESULT_SIZE]; 

unsigned int index_a = 0; 
unsigned int index_b = 0; 
unsigned int index_result = 0; 
for (index_result = 0; index_result < RESULT_SIZE;) 
{ 
    result[index_result++] = B[index_b++]; 
    result[index_result++] = A[index_a++]; 
    result[index_result++] = A[index_a++]; 
} 

odwijanie pętli powtórzy treść "standardowego" for pętli:

for (index_result = 0; index_result < RESULT_SIZE;) 
{ 
    result[index_result++] = B[index_b++]; 
    result[index_result++] = A[index_a++]; 
    result[index_result++] = A[index_a++]; 

    result[index_result++] = B[index_b++]; 
    result[index_result++] = A[index_a++]; 
    result[index_result++] = A[index_a++]; 
} 

w wersji rozwinął, liczbę pętle zostały przecięte na pół.

Poprawa wydajności może być nieistotna w porównaniu do innych opcji. następujące kwestie wpływać na wydajność i każdy z nich może mieć różne ulepszenia prędkość:

  • tworzenie cache dane strzela
  • przeładunku rurociągu instrukcji (w zależności od procesora)
  • System operacyjny zamiana pamięci z dysku
  • innych zadań działa równolegle
  • Przetwarzanie równoległe (w zależności od procesora/platformy)

Jednym z przykładów przetwarzania równoległego jest to, że jeden procesor kopiuje elementy B do nowej tablicy, a inny procesor kopiuje elementy A do nowej tablicy.

+0

Dzięki za życzliwą odpowiedź. Zmieniłem mój post, aby lepiej wyjaśnić problem. Czy myślisz, że poprzez "#pragma unroll" mam szansę poprawić sytuację? Nie wiem, ponieważ wszystko na temat kopii jest znane w czasie wykonywania. – JackOLantern

+0

Jak już powiedziałem, zależy to od procesora. W przypadku niektórych procesorów gałąź wypróżnia potok instrukcji, a procesor musi go ponownie wczytać. Niektóre współczesne procesory mają wystarczającą pamięć podręczną potoków instrukcji, które przechowują proste pętle for w pamięci podręcznej instrukcji i nie muszą przeładowywać. –

+0

Większość procesorów woli wykonywać instrukcje sekwencyjne i wolałaby nie napotykać instrukcji oddziału. –

6

Może to być odpowiedź zbyt szczegółowa, ale na platformie ARM obsługującej NEON wektoryzacja NEON może być jeszcze szybsza. Może to być ratowanie życia w środowisku, w którym zasoby są względnie bardziej ograniczone, i prawdopodobnie dlatego ARM jest używany w tym ustawieniu w pierwszej kolejności. Ważnym przykładem jest system Android, na którym większość urządzeń nadal korzysta z architektury ARM v7a, która obsługuje NEON.

Poniższe przykłady pokazują, że jest to pętla do kopiowania półpłaszczyznowej płaszczyzny UV obrazu YUV420sp do płaskiej płaszczyzny UV obrazu YUV420p. Rozmiary buforów źródłowego i docelowego są równe 640*480/2 bajtów. Wszystkie przykłady są kompilowane za pomocą g ++ 4.8 wewnątrz Android NDK r9d. Są one wykonywane na procesorze Samsung Exynos 5420 Octa:

Poziom 1: Regular

void convertUVsp2UVp(
    unsigned char* __restrict srcptr, 
    unsigned char* __restrict dstptr, 
    int stride) 
{ 
    for(int i=0;i<stride;i++){ 
     dstptr[i]   = srcptr[i*2]; 
     dstptr[i + stride] = srcptr[i*2 + 1]; 
    } 
} 

skompilowany z -O3 tylko, zajmuje około 1,5 ms przeciętnie.

Poziom 2: Rozwinięta i wyciska trochę więcej z ruchomymi wskaźnikami

void convertUVsp2UVp(
    unsigned char* __restrict srcptr, 
    unsigned char* __restrict dstptr, 
    int stride) 
{ 
    unsigned char* endptr = dstptr + stride; 
    while(dstptr<endptr){ 
     *(dstptr + 0)    = *(srcptr + 0); 
     *(dstptr + stride + 0) = *(srcptr + 1); 
     *(dstptr + 1)    = *(srcptr + 2); 
     *(dstptr + stride + 1) = *(srcptr + 3); 
     *(dstptr + 2)    = *(srcptr + 4); 
     *(dstptr + stride + 2) = *(srcptr + 5); 
     *(dstptr + 3)    = *(srcptr + 6); 
     *(dstptr + stride + 3) = *(srcptr + 7); 
     *(dstptr + 4)    = *(srcptr + 8); 
     *(dstptr + stride + 4) = *(srcptr + 9); 
     *(dstptr + 5)    = *(srcptr + 10); 
     *(dstptr + stride + 5) = *(srcptr + 11); 
     *(dstptr + 6)    = *(srcptr + 12); 
     *(dstptr + stride + 6) = *(srcptr + 13); 
     *(dstptr + 7)    = *(srcptr + 14); 
     *(dstptr + stride + 7) = *(srcptr + 15); 
     srcptr+=16; 
     dstptr+=8; 
    } 
} 

skompilowany z -O3 tylko, zajmuje około 1,15 ms przeciętnie. Prawdopodobnie jest to tak szybkie jak w przypadku zwykłej architektury, jak w przypadku innej odpowiedzi.

Poziom 3 Zwykły + GCC automatyczne NEON wektoryzacji

void convertUVsp2UVp(
    unsigned char* __restrict srcptr, 
    unsigned char* __restrict dstptr, 
    int stride) 
{ 
    for(int i=0;i<stride;i++){ 
     dstptr[i]   = srcptr[i*2]; 
     dstptr[i + stride] = srcptr[i*2 + 1]; 
    } 
} 

skompilowany z -O3 -mfpu=neon -ftree-vectorize -ftree-vectorizer-verbose=1 -mfloat-abi=softfp, trwa około 0,6 ms na średniej. Dla porównania, bajty o rozmiarze równym lub dwa razy większym niż to, które tu testowano, zajmują średnio około 0,6 ms.

Na marginesie, drugi kod (rozwijany i wskazywany) skompilowany z powyższymi parametrami NEON zajmuje mniej więcej tyle samo czasu, 0,6 ms.