En combinant les réponses de Juste un poseur et de Jitamaro, si vous supposez que les entrées et les sorties sont alignées sur 16 octets et si vous traitez les pixels 4 par 4, vous pouvez utiliser une combinaison de shuffles, de masques, de ands et de ors pour stocker en utilisant des magasins alignés. L'idée principale est de générer quatre ensembles de données intermédiaires, puis de les réunir avec des masques pour sélectionner les valeurs de pixels pertinentes et écrire 3 ensembles de données de pixels de 16 octets. Notez que je n'ai pas compilé ce programme ni essayé de l'exécuter.
EDIT2 : Plus de détails sur la structure du code sous-jacent :
Avec SSE2, vous obtenez de meilleures performances avec des lectures et des écritures de 16 octets alignés. Puisque votre pixel de 3 octets ne peut être aligné que sur 16 octets tous les 16 pixels, nous regroupons 16 pixels à la fois en utilisant une combinaison de shuffles et de masques et ors de 16 pixels d'entrée à la fois.
Du LSB au MSB, les entrées ressemblent à ceci, en ignorant les composants spécifiques :
s[0]: 0000 0000 0000 0000
s[1]: 1111 1111 1111 1111
s[2]: 2222 2222 2222 2222
s[3]: 3333 3333 3333 3333
et les ouptuts ressemblent à ça :
d[0]: 000 000 000 000 111 1
d[1]: 11 111 111 222 222 22
d[2]: 2 222 333 333 333 333
Pour générer ces sorties, vous devez donc procéder comme suit (je préciserai les transformations réelles plus tard) :
d[0]= combine_0(f_0_low(s[0]), f_0_high(s[1]))
d[1]= combine_1(f_1_low(s[1]), f_1_high(s[2]))
d[2]= combine_2(f_1_low(s[2]), f_1_high(s[3]))
Maintenant, qu'est-ce qui devrait combine_<x>
ressemble-t-il ? Si nous supposons que d
est simplement s
compactés ensemble, nous pouvons concaténer deux s
avec un masque et un ou :
combine_x(left, right)= (left & mask(x)) | (right & ~mask(x))
où (1 signifie sélectionner le pixel de gauche, 0 signifie sélectionner le pixel de droite) : mask(0)= 111 111 111 111 000 0 mask(1)= 11 111 111 000 000 00 masque(2)= 1 111 000 000 000 000
Mais les transformations réelles ( f_<x>_low
, f_<x>_high
) ne sont en fait pas si simples. Puisque nous inversons et supprimons des octets du pixel source, la transformation réelle est (pour la première destination par souci de brièveté) :
d[0]=
s[0][0].Blue s[0][0].Green s[0][0].Red
s[0][1].Blue s[0][1].Green s[0][1].Red
s[0][2].Blue s[0][2].Green s[0][2].Red
s[0][3].Blue s[0][3].Green s[0][3].Red
s[1][0].Blue s[1][0].Green s[1][0].Red
s[1][1].Blue
Si vous traduisez ce qui précède en décalages d'octets de la source à la destination, vous obtenez : d[0]= &s[0]+3 &s[0]+2 &s[0]+1
&s[0]+7 &s[0]+6 &s[0]+5 &s[0]+11 &s[0]+10 &s[0]+9 &s[0]+15 &s[0]+14 &s[0]+13
&s[1]+3 &s[1]+2 &s[1]+1
&s[1]+7
(Si vous jetez un coup d'oeil à tous les décalages de s[0], ils correspondent juste au masque de brassage d'un poseur dans l'ordre inverse).
Maintenant, nous pouvons générer un masque de brassage pour faire correspondre chaque octet source à un octet de destination ( X
signifie que nous ne nous soucions pas de cette valeur) :
f_0_low= 3 2 1 7 6 5 11 10 9 15 14 13 X X X X
f_0_high= X X X X X X X X X X X X 3 2 1 7
f_1_low= 6 5 11 10 9 15 14 13 X X X X X X X X
f_1_high= X X X X X X X X 3 2 1 7 6 5 11 10
f_2_low= 9 15 14 13 X X X X X X X X X X X X
f_2_high= X X X X 3 2 1 7 6 5 11 10 9 15 14 13
Nous pouvons encore l'optimiser en examinant les masques que nous utilisons pour chaque pixel source. Si vous regardez les masques de brassage que nous utilisons pour s[1] :
f_0_high= X X X X X X X X X X X X 3 2 1 7
f_1_low= 6 5 11 10 9 15 14 13 X X X X X X X X
Puisque les deux masques de brassage ne se chevauchent pas, nous pouvons les combiner et simplement masquer les pixels non pertinents dans combine_, ce que nous avons déjà fait ! Le code suivant effectue toutes ces optimisations (il suppose en outre que les adresses source et destination sont alignées sur 16 octets). De plus, les masques sont écrits dans le code dans l'ordre MSB->LSB, au cas où vous seriez confus quant à l'ordre.
EDIT : changement de magasin en _mm_stream_si128
puisque vous faites probablement beaucoup d'écritures et que nous ne voulons pas nécessairement vider le cache. De plus, il devrait être aligné de toute façon, ce qui vous permet de bénéficier d'une perforation gratuite !
#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>
// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
assert((uintptr_t)orig % 16 == 0);
assert(imagesize % 16 == 0);
__m128i shuf0 = _mm_set_epi8(
-128, -128, -128, -128, // top 4 bytes are not used
13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3); // bottom 12 go to the first pixel
__m128i shuf1 = _mm_set_epi8(
7, 1, 2, 3, // top 4 bytes go to the first pixel
-128, -128, -128, -128, // unused
13, 14, 15, 9, 10, 11, 5, 6); // bottom 8 go to second pixel
__m128i shuf2 = _mm_set_epi8(
10, 11, 5, 6, 7, 1, 2, 3, // top 8 go to second pixel
-128, -128, -128, -128, // unused
13, 14, 15, 9); // bottom 4 go to third pixel
__m128i shuf3 = _mm_set_epi8(
13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3, // top 12 go to third pixel
-128, -128, -128, -128); // unused
__m128i mask0 = _mm_set_epi32(0, -1, -1, -1);
__m128i mask1 = _mm_set_epi32(0, 0, -1, -1);
__m128i mask2 = _mm_set_epi32(0, 0, 0, -1);
uint8_t *end = orig + imagesize * 4;
for (; orig != end; orig += 64, dest += 48) {
__m128i a= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), shuf0);
__m128i b= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 1), shuf1);
__m128i c= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 2), shuf2);
__m128i d= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 3), shuf3);
_mm_stream_si128((__m128i *)dest, _mm_or_si128(_mm_and_si128(a, mask0), _mm_andnot_si128(b, mask0));
_mm_stream_si128((__m128i *)dest + 1, _mm_or_si128(_mm_and_si128(b, mask1), _mm_andnot_si128(c, mask1));
_mm_stream_si128((__m128i *)dest + 2, _mm_or_si128(_mm_and_si128(c, mask2), _mm_andnot_si128(d, mask2));
}
}