8
votes

Méthode plus rapide pour extraire et combiner des bits de UINT16 à UINT8

Je recherche un moyen plus rapide pour mon extrait spécial requis et l'opération de combinaison comme décrit ci-dessous:

PairFlags |= (ChannelFlags & 0x0003) ? 0x0001 : 0;
PairFlags |= (ChannelFlags & 0x000C) ? 0x0002 : 0;
PairFlags |= (ChannelFlags & 0x0030) ? 0x0004 : 0;
PairFlags |= (ChannelFlags & 0x00C0) ? 0x0008 : 0;
PairFlags |= (ChannelFlags & 0x0300) ? 0x0010 : 0;
PairFlags |= (ChannelFlags & 0x0C00) ? 0x0020 : 0;
PairFlags |= (ChannelFlags & 0x3000) ? 0x0040 : 0;
PairFlags |= (ChannelFlags & 0xC000) ? 0x0080 : 0;

Par souci de simplicité, ci-dessus n'est qu'un exemple 8 bits, il en va de même pour les valeurs 16 bits. Il doit être implémenté le plus rapidement possible sur le microcontrôleur dsPIC33F.

Le moyen le plus simple en C est:

+-------+-------+-------+-------+-------+-------+-------+-------+
| BIT 7 | BIT 6 | BIT 5 | BIT 4 | BIT 3 | BIT 2 | BIT 1 | BIT 0 |
+-------+-------+-------+-------+-------+-------+-------+-------+
|   D1  |  D0   |  C1   |  C0   |  B1   |  B0   |  A1   |   A0  |
+-------+-------+-------+-------+-------+-------+-------+-------+

A = A0 OR A1
B = B0 OR B1
C = C0 OR C1
D = D0 OR D1

+-------+-------+-------+-------+-------+-------+-------+-------+
| BIT 7 | BIT 6 | BIT 5 | BIT 4 | BIT 3 | BIT 2 | BIT 1 | BIT 0 |
+-------+-------+-------+-------+-------+-------+-------+-------+
|       |       |       |       |   D   |   C   |   B   |   A   |
+-------+-------+-------+-------+-------+-------+-------+-------+

Cela produira env. 40 instructions (avec O3) ce qui correspond à 1 µs dans mon cas.

Le nombre de cycles d'instruction doit être réduit si possible. Existe-t-il un moyen plus rapide en C ou en assemblage en ligne?


10 commentaires

Le nombre d'instructions ou le nombre d'agences est-il le principal souci de performance?


@Lundin Le nombre de cycles d'instruction est important


Je suppose qu'un dsPIC a toutes sortes de prédiction de branche sophistiquée?


@Lundin Je n'ai jamais entendu dire que dsPIC33F a implémenté des algorithmes de prédiction de branche sophistiqués.


Je ne sais pas si c'est une solution compétitive en termes de performances, mais vous pouvez le faire avec une simple recherche de table - aucun asm n'est nécessaire.


@ 500-InternalServerError Je commence à penser que ce serait aussi la meilleure solution.


Une table de recherche pour le mot source entier (gardez à l'esprit que nous parlons ici de 16 bits) ou par quartet de 2 bits? Plus tard, il suffit de reconstruire l'instruction "OR" dans le logiciel en tant que LUT.


Vous pouvez probablement créer une table de 256 octets pour la version 8 bits, puis l'appeler par octet dans la version 16 bits?


Hmm ... une table de consultation en flash causerait-elle un hoquet d'architecture de Harvard sur cette partie? Il serait idéalement de 256 octets.


@Lundin: En effet, en utilisant la première étape de la réponse d'Ian, un autre shift / OR / truncate-to-8bit ne vous laisse qu'un problème de 8 bits bit-shuffle, voir mon commentaire . Si une LUT de 256 octets est bonne, ce serait la voie à suivre.


4 Réponses :


4
votes

Je ne sais pas si c'est plus efficace mais au lieu d'utiliser un si ternaire, pourquoi ne pas utiliser uniquement des opérations au niveau du bit? Et juste compenser avec l'opérateur bitshift

PairFlags = ((ChannelFlags & (0b1 << 0)) | (ChannelFlags & (0b10 << 0))) << 0;
PairFlags = ((ChannelFlags & (0b1 << 2)) | (ChannelFlags & (0b10 << 2))) << 1;
PairFlags = ((ChannelFlags & (0b1 << 4)) | (ChannelFlags & (0b10 << 4))) << 2;
//...


0 commentaires

6
votes

En supposant que tout soit bien (non testé), cela semble générer du bon code sans branche au moins sur gcc et clang pour x86 (-O3):

convert:                                # @convert
        test    dil, 3
        setne   al
        test    dil, 12
        setne   cl
        add     cl, cl
        or      cl, al
        test    dil, 48
        setne   al
        shl     al, 2
        or      al, cl
        mov     ecx, edi
        shr     cl, 7
        shr     dil, 6
        and     dil, 1
        or      dil, cl
        shl     dil, 3
        or      al, dil
        ret

Cela masque chaque ensemble de bits individuel, puis vérifie par rapport à zéro pour finir avec 1 ou 0 dans un int temporaire. Cette valeur est décalée en position dans le résultat, avant que tout soit finalement au niveau du bit OU: édité ensemble. Code complet:

#include <stdint.h>

#define A1A0  (3u << 0)
#define B1B0  (3u << 2)
#define C1C0  (3u << 4)
#define D1D0  (3u << 6)

#define A_POS 0
#define B_POS 1
#define C_POS 2
#define D_POS 3

uint8_t convert (uint8_t ChannelFlags)
{
  return ( ((ChannelFlags & A1A0)!=0) << A_POS ) |
         ( ((ChannelFlags & B1B0)!=0) << B_POS ) |
         ( ((ChannelFlags & C1C0)!=0) << C_POS ) |
         ( ((ChannelFlags & D1D0)!=0) << D_POS ) ;  
}

clang démontage x86 donne 18 instructions sans branche:

uint8_t convert (uint8_t ChannelFlags)
{
  return ( ((ChannelFlags & A1A0)!=0) << A_POS ) |
         ( ((ChannelFlags & B1B0)!=0) << B_POS ) |
         ( ((ChannelFlags & C1C0)!=0) << C_POS ) |
         ( ((ChannelFlags & D1D0)!=0) << D_POS ) ;  
}


3 commentaires

Je suppose que vous mentez #define A_POS (0), #define B_POS (1) ... Dans tous les cas, cela revient exactement à mon chemin C donné qui n'est malheureusement pas rapide (étant donné que 16 bits sont convertis).


@bkausbk Oh oui, c'est un bug. Attends, je vais éditer.


@bkausbk Fixe. Il s'avère que la deuxième version que j'ai publiée a donné un meilleur code machine lorsque les masques de bits ont été corrigés.



2
votes

Voici une idée. Observez une chose ici:

PairFlags = (PairFlags | (PairFlags >> 1))
PairFlags = (PairFlags&1) | ((PairFlags&4)>>1) | ((PairFlags&16)>>2) | ((PairFlags&64)>>3)

Vous avez 4 ou opérations. Vous pouvez tous les exécuter en une seule instruction:

[D1][D1 or D0][D0 or C1][C1 or C0][C0 or B1][B1 or B0][B0 or A1][A1 or A0]

Maintenant, vos bits sont alignés comme ça:

PairFlags = (PairFlags | (PairFlags >> 1))

Il vous suffit donc d'extraire les bits 0, 2, 4, 6 pour obtenir le résultat.

Bit 0. Est déjà OK.

Le bit 1 doit être défini sur le bit 2.

Le bit 2 doit être défini sur le bit 4.

Le bit 3 doit être défini sur le bit 6.

Code final quelque chose comme ça:

A = A0 OR A1
B = B0 OR B1
C = C0 OR C1
D = D0 OR D1


1 commentaires

En effet une méthode intelligente. Il compile un peu moins d'instructions (26) et est sans branche.



8
votes

Ce qui suit devrait fonctionner pour réduire une valeur de 16 bits à 8 bits (avec chaque bit de sortie formé par OU une paire de bits d'entrée):

// Set even bits to bits in pair ORed together, and odd bits to 0...
PairFlags = (ChannelFlags | (ChannelFlags >> 1)) & 0x5555; // '0h0g0f0e0d0c0b0a'
// Compress the '00' or '01' bit pairs down to single '0' or '1' bits...
PairFlags = (PairFlags ^ (PairFlags >> 1)) & 0x3333; // '00hg00fe00dc00ba'
PairFlags = (PairFlags ^ (PairFlags >> 2)) & 0x0F0F; // '0000hgfe0000dcba'
PairFlags = (PairFlags ^ (PairFlags >> 4)) & 0x00FF; // '00000000hgfedcba'

Remarque: Le ^ peut être remplacé par | ci-dessus pour le même résultat.


3 commentaires

C'est exactement ce que je cherchais, je ne l'ai pas encore testé, mais il a été compilé avec seulement 15 instructions sans branche.


@bkausbk: Si les tables de recherche sont efficaces, utilisez la première étape de cette réponse, puis transformez 0h0g0f0e0d0c0b0a en hdgcfbea en faisant PairFlags |= PairFlags >> 7 et en prenant l'octet de PairFlags |= PairFlags >> 7 faible. ( (uint8_t) ou & 0xFF ). Ensuite, une LUT 256 x 8 bits peut effectuer le bit-shuffle pour donner les bits dans l'ordre souhaité. Sur un processeur x86 moderne, 3 étapes supplémentaires shift / xor / et étapes seraient probablement plus rapides qu'une table de recherche (sauf peut-être avec une utilisation intelligente de SSSE3 pshufb pour dbca -> dcba nibble dcba ), mais si une charge est garantie de ne prendre qu'un quelques cycles (pas de manque de cache possible) et 256B d'espace table sont bon marché, essayez-le.


Bien sûr, sur les processeurs Intel récents (qui ont un pext pext rapide, contrairement à AMD récent où il est lent uops.info ), vous n'avez besoin que de _pext_u32( ChannelFlags | (ChannelFlags << 1), 0xAAAA ) . Comme 3 instructions asm (lea / OR / pext), ou 4 incluant un mov -immediate pour configurer la constante. Le décalage gauche au lieu de droite permet de le faire en utilisant lea pour éviter de détruire l'opérande source, au lieu de mov + shl.