66 votes

Méthode rapide pour copier la mémoire avec traduction - ARGB vers BGR

Vue d'ensemble

J'ai un tampon d'image que je dois convertir dans un autre format. Le tampon d'origine est composé de quatre canaux, 8 bits par canal, Alpha, Rouge, Vert et Bleu. Le tampon de destination est composé de trois canaux, 8 bits par canal, bleu, vert et rouge.

Donc la méthode de force brute est :

// Assume a 32 x 32 pixel image
#define IMAGESIZE (32*32)

typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

ARGB orig[IMAGESIZE];
BGR  dest[IMAGESIZE];

for(x = 0; x < IMAGESIZE; x++)
{
     dest[x].Red = orig[x].Red;
     dest[x].Green = orig[x].Green;
     dest[x].Blue = orig[x].Blue;
}

Cependant, j'ai besoin de plus de rapidité que celle fournie par une boucle et des copies de trois octets. J'espère qu'il y a quelques astuces que je peux utiliser pour réduire le nombre de lectures et d'écritures en mémoire, étant donné que je fonctionne sur une machine 32 bits.

Informations complémentaires

Chaque image est un multiple d'au moins 4 pixels. Nous pourrions donc adresser 16 octets ARGB et les déplacer dans 12 octets RGB par boucle. Peut-être ce fait peut-il être utilisé pour accélérer les choses, d'autant plus qu'il s'inscrit parfaitement dans les limites de 32 bits.

J'ai accès à OpenCL - et bien que cela nécessite de déplacer l'ensemble du tampon dans la mémoire du GPU, puis d'en ressortir le résultat, le fait qu'OpenCL puisse travailler sur de nombreuses portions de l'image simultanément, et le fait que les déplacements de gros blocs de mémoire soient en fait assez efficaces peuvent rendre cette exploration intéressante.

Bien que j'aie donné l'exemple de petits tampons ci-dessus, je déplace réellement des vidéos HD (1920x1080) et parfois des tampons plus grands, la plupart du temps plus petits, donc si une situation 32x32 peut être triviale, copier 8,3 Mo de données d'image octet par octet est vraiment, vraiment mauvais.

Il fonctionne sur des processeurs Intel (Core 2 et plus) et il y a donc des commandes de traitement de données et de streaming dont je suis conscient de l'existence, mais que je ne connais pas - peut-être que des pointeurs sur où chercher des instructions spécialisées de traitement de données seraient bons.

Cela va être intégré dans une application OS X, et j'utilise XCode 4. Si l'assemblage est sans douleur et la solution évidente, je suis d'accord pour suivre cette voie, mais ne l'ayant pas fait sur cette configuration auparavant, je me méfie d'y consacrer trop de temps.

Un pseudo-code est acceptable - je ne cherche pas une solution complète, juste l'algorithme et une explication de toute astuce qui pourrait ne pas être immédiatement claire.

56voto

ughoavgfhw Points 28400

J'ai écrit 4 versions différentes qui fonctionnent par échange d'octets. Je les ai compilées en utilisant gcc 4.2.1 avec -O3 -mssse3 Nous les avons exécutés 10 fois sur 32MB de données aléatoires et avons trouvé les moyennes.


Note de l'éditeur : l'asm inline original utilisait des contraintes non sûres, par exemple en modifiant les opérandes en entrée seulement, sans en informer le compilateur. l'effet secondaire sur la mémoire pointée par les entrées de pointeurs dans les registres . Apparemment, cela a bien fonctionné pour le benchmark. J'ai corrigé les contraintes pour qu'elles soient correctement sécurisées pour tous les appelants. Cela ne devrait pas affecter les chiffres du benchmark, mais seulement s'assurer que le code environnant est sûr pour tous les appelants. Les CPUs modernes avec une bande passante mémoire plus élevée devraient voir une plus grande accélération pour SIMD par rapport à scalaire 4-byte-at-a-time, mais les plus grands avantages sont quand les données sont chaudes dans le cache (travail dans des blocs plus petits, ou sur des tailles totales plus petites).

En 2020, votre meilleure chance est d'utiliser le portable _mm_loadu_si128 version intrinsèque qui sera compilée dans une boucle asm équivalente : https://gcc.gnu.org/wiki/DontUseInlineAsm .

Notez également que toutes ces opérations écrasent 1 (scalaire) ou 4 (SIMD) octets après la fin de la sortie, donc faites les 3 derniers octets séparément si cela pose un problème.

--- @PeterCordes


La première version utilise une boucle C pour convertir chaque pixel séparément, en utilisant la fonction OSSwapInt32 (qui se compile en une fonction bswap l'instruction avec -O3 ).

void swap1(ARGB *orig, BGR *dest, unsigned imageSize) {
    unsigned x;
    for(x = 0; x < imageSize; x++) {
        *((uint32_t*)(((uint8_t*)dest)+x*3)) = OSSwapInt32(((uint32_t*)orig)[x]);
        // warning: strict-aliasing UB.  Use memcpy for unaligned loads/stores
    }
}

La deuxième méthode effectue la même opération, mais utilise une boucle d'assemblage en ligne au lieu d'une boucle C.

void swap2(ARGB *orig, BGR *dest, unsigned imageSize) {
    asm volatile ( // has to be volatile because the output is a side effect on pointed-to memory
        "0:\n\t"                   // do {
        "movl   (%1),%%eax\n\t"
        "bswapl %%eax\n\t"
        "movl   %%eax,(%0)\n\t"    // copy a dword byte-reversed
        "add    $4,%1\n\t"         // orig += 4 bytes
        "add    $3,%0\n\t"         // dest += 3 bytes
        "dec    %2\n\t"
        "jnz    0b"                // }while(--imageSize)
        : "+r" (dest), "+r" (orig), "+r" (imageSize)
        : // no pure inputs; the asm modifies and dereferences the inputs to use them as read/write outputs.
        : "flags", "eax", "memory"
    );
}

La troisième version est une version modifiée de juste la réponse d'un poseur . J'ai converti les fonctions intégrées en leurs équivalents GCC et j'ai utilisé la fonction lddqu de sorte que l'argument d'entrée n'a pas besoin d'être aligné. (Note de l'éditeur : seule la P4 a bénéficié de la fonction lddqu ; il est possible d'utiliser movdqu mais il n'y a pas d'inconvénient).

typedef char v16qi __attribute__ ((vector_size (16)));
void swap3(uint8_t *orig, uint8_t *dest, size_t imagesize) {
    v16qi mask = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};
    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 16, dest += 12) {
        __builtin_ia32_storedqu(dest,__builtin_ia32_pshufb128(__builtin_ia32_lddqu(orig),mask));
    }
}

Enfin, la quatrième version est l'équivalent en montage en ligne de la troisième.

void swap2_2(uint8_t *orig, uint8_t *dest, size_t imagesize) {
    static const int8_t mask[16] = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};
    asm volatile (
        "lddqu  %3,%%xmm1\n\t"
        "0:\n\t"
        "lddqu  (%1),%%xmm0\n\t"
        "pshufb %%xmm1,%%xmm0\n\t"
        "movdqu %%xmm0,(%0)\n\t"
        "add    $16,%1\n\t"
        "add    $12,%0\n\t"
        "sub    $4,%2\n\t"
        "jnz    0b"
        : "+r" (dest), "+r" (orig), "+r" (imagesize)
        : "m" (mask)  // whole array as a memory operand.  "x" would get the compiler to load it
        : "flags", "xmm0", "xmm1", "memory"
    );
}

(Ces compile bien avec GCC9.3 mais clang10 ne sait pas __builtin_ia32_pshufb128 ; utiliser _mm_shuffle_epi8 .)

Sur mon MacBook Pro 2010, 2.4 Ghz i5 (Westmere/Arrandale), 4GB RAM, voici les temps moyens pour chacun :

Version 1: 10.8630 milliseconds
Version 2: 11.3254 milliseconds
Version 3:  9.3163 milliseconds
Version 4:  9.3584 milliseconds

Comme vous pouvez le constater, le compilateur est suffisamment performant en matière d'optimisation pour que vous n'ayez pas besoin d'écrire l'assembleur. De plus, les fonctions vectorielles ne sont plus rapides que de 1,5 milliseconde sur 32 Mo de données, ce qui n'est pas très grave si vous voulez prendre en charge les premiers macs Intel, qui ne supportaient pas SSSE3.

Edit : liori a demandé des informations sur l'écart-type. Malheureusement, je n'avais pas sauvegardé les points de données, donc j'ai fait un autre test avec 25 itérations.

              Average    | Standard Deviation
Brute force: 18.01956 ms | 1.22980 ms (6.8%)
Version 1:   11.13120 ms | 0.81076 ms (7.3%)
Version 2:   11.27092 ms | 0.66209 ms (5.9%)
Version 3:    9.29184 ms | 0.27851 ms (3.0%)
Version 4:    9.40948 ms | 0.32702 ms (3.5%)

Voici également les données brutes des nouveaux tests, au cas où quelqu'un les voudrait. Pour chaque itération, un ensemble de données de 32MB a été généré aléatoirement et exécuté par les quatre fonctions. Le temps d'exécution de chaque fonction en microsecondes est indiqué ci-dessous.

Brute force: 22173 18344 17458 17277 17508 19844 17093 17116 19758 17395 18393 17075 17499 19023 19875 17203 16996 17442 17458 17073 17043 18567 17285 17746 17845
Version 1:   10508 11042 13432 11892 12577 10587 11281 11912 12500 10601 10551 10444 11655 10421 11285 10554 10334 10452 10490 10554 10419 11458 11682 11048 10601
Version 2:   10623 12797 13173 11130 11218 11433 11621 10793 11026 10635 11042 11328 12782 10943 10693 10755 11547 11028 10972 10811 11152 11143 11240 10952 10936
Version 3:    9036  9619  9341  8970  9453  9758  9043 10114  9243  9027  9163  9176  9168  9122  9514  9049  9161  9086  9064  9604  9178  9233  9301  9717  9156
Version 4:    9339 10119  9846  9217  9526  9182  9145 10286  9051  9614  9249  9653  9799  9270  9173  9103  9132  9550  9147  9157  9199  9113  9699  9354  9314

25voto

just a poseur Points 741

L'évident, en utilisant pshufb.

#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 % 4 == 0);
    __m128i mask = _mm_set_epi8(-128, -128, -128, -128, 13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3);
    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 16, dest += 12) {
        _mm_storeu_si128((__m128i *)dest, _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), mask));
    }
}

16voto

MSN Points 30386

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));
    }
}

11voto

eznme Points 13158

J'arrive un peu tard à la fête, il semble que la communauté se soit déjà décidée pour la réponse pshufb de poseur mais distribuer 2000 de réputation, c'est si extrêmement généreux que je dois essayer.

Voici ma version sans intrinsèque spécifique à la plate-forme ou asm spécifique à la machine, j'ai inclus un code de synchronisation multiplateforme montrant une 4 fois plus rapide si tu fais les deux comme moi. ET activer l'optimisation du compilateur (optimisation des registres, déroulement des boucles) :

#include "stdlib.h"
#include "stdio.h"
#include "time.h"

#define UInt8 unsigned char

#define IMAGESIZE (1920*1080) 
int main() {
    time_t  t0, t1;
    int frames;
    int frame; 
    typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
    typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

    ARGB* orig = malloc(IMAGESIZE*sizeof(ARGB));
    if(!orig) {printf("nomem1");}
    BGR* dest = malloc(IMAGESIZE*sizeof(BGR));
    if(!dest) {printf("nomem2");}

    printf("to start original hit a key\n");
    getch();
    t0 = time(0);
    frames = 1200;
    for(frame = 0; frame<frames; frame++) {
        int x; for(x = 0; x < IMAGESIZE; x++) {
            dest[x].Red = orig[x].Red;
            dest[x].Green = orig[x].Green;
            dest[x].Blue = orig[x].Blue;
            x++;
        }
    }
    t1 = time(0);
    printf("finished original of %u frames in %u seconds\n", frames, t1-t0);

    // on my core 2 subnotebook the original took 16 sec 
    // (8 sec with compiler optimization -O3) so at 60 FPS 
    // (instead of the 1200) this would be faster than realtime 
    // (if you disregard any other rendering you have to do). 
    // However if you either want to do other/more processing 
    // OR want faster than realtime processing for e.g. a video-conversion 
    // program then this would have to be a lot faster still.

    printf("to start alternative hit a key\n");
    getch();
    t0 = time(0);
    frames = 1200;
    unsigned int* reader;
    unsigned int* end = reader+IMAGESIZE;
    unsigned int cur; // your question guarantees 32 bit cpu
    unsigned int next;
    unsigned int temp;
    unsigned int* writer;
    for(frame = 0; frame<frames; frame++) {
        reader = (void*)orig;
        writer = (void*)dest;
        next = *reader;
        reader++;
        while(reader<end) {
            cur = next;
            next = *reader;         
            // in the following the numbers are of course the bitmasks for 
            // 0-7 bits, 8-15 bits and 16-23 bits out of the 32
            temp = (cur&255)<<24 | (cur&65280)<<16|(cur&16711680)<<8|(next&255); 
            *writer = temp;
            reader++;
            writer++;
            cur = next;
            next = *reader;
            temp = (cur&65280)<<24|(cur&16711680)<<16|(next&255)<<8|(next&65280);
            *writer = temp;
            reader++;
            writer++;
            cur = next;
            next = *reader;
            temp = (cur&16711680)<<24|(next&255)<<16|(next&65280)<<8|(next&16711680);
            *writer = temp;
            reader++;
            writer++;
        }
    }
    t1 = time(0);
    printf("finished alternative of %u frames in %u seconds\n", frames, t1-t0);

    // on my core 2 subnotebook this alternative took 10 sec 
    // (4 sec with compiler optimization -O3)

}

Les résultats sont les suivants (sur mon subnotebook core 2) :

F:\>gcc b.c -o b.exe

F:\>b
to start original hit a key
finished original of 1200 frames in 16 seconds
to start alternative hit a key
finished alternative of 1200 frames in 10 seconds

F:\>gcc b.c -O3 -o b.exe

F:\>b
to start original hit a key
finished original of 1200 frames in 8 seconds
to start alternative hit a key
finished alternative of 1200 frames in 4 seconds

7voto

Phpdna Points 8166

Vous voulez utiliser l'appareil d'un Duff : http://en.wikipedia.org/wiki/Duff%27s_device . Il fonctionne également en JavaScript. Ce post est cependant un peu drôle à lire. http://lkml.indiana.edu/hypermail/linux/kernel/0008.2/0171.html . Imaginez un dispositif Duff avec 512 Kbytes de mouvements.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X