2 votes

Pointeur de fonction comme "tag" dans l'union étiquetée

L'une des raisons pour lesquelles j'ai tendance à éviter d'utiliser les unions étiquetées est que je n'aime pas l'idée de la pénalité de performance qu'entraîne l'utilisation d'une union étiquetée. switch/case pour la balise pourrait être introduite si le nombre de balises est supérieur à 4 environ.

Je viens d'avoir l'idée qu'au lieu d'utiliser une balise, on pourrait placer un pointeur sur une fonction qui lirait la dernière valeur écrite dans le fichier union . Par exemple :

union u{
   char a;
   int b;
   size_t c;
};

struct fntag{
   void (*readval)(union u *ptr, void *out);
   union u val;
};

Ensuite, chaque fois que vous écrivez une valeur dans val vous mettez également à jour le readval en conséquence, de sorte qu'il pointe vers une fonction qui lit le dernier champ que vous avez écrit dans l'union. Oui, il y a un problème difficile, et c'est où renvoyer la valeur lue (parce que le même pointeur de fonction ne peut pas pointer vers des fonctions renvoyant des types différents). J'ai choisi de renvoyer la valeur par le biais d'un pointeur vers void de sorte que cette fonction puisse également être "surchargée" avec C11 _Generic() et donc de couler et d'écrire dans des types différents pour la sortie.

Bien sûr, l'invocation d'un pointeur vers une fonction a un surcoût en termes de performances, et je suppose qu'il est bien plus lourd que la vérification de la valeur d'une fonction enum mais à un moment donné, si le nombre de tags est important, je pense qu'il sera plus rapide que switch/case .

Ma question est la suivante : avez-vous déjà vu cette technique utilisée quelque part ? (Je ne l'ai pas fait, et je ne sais pas si c'est parce que l'application de cette technique dans le monde réel nécessiterait de l'argent. _Generic() sur la fonction readval, qui nécessite C11, ou si c'est à cause d'un problème que je ne remarque pas pour l'instant). Combien de tags pensez-vous qu'il faudrait avoir pour que le pointeur fonctionne plus rapidement - dans les processeurs Intel actuels - que la fonction switch/case ?

2voto

ThorX89 Points 967

Vous pouvez le faire. Dans votre cas, une signature de fonction plus facile à optimiser serait size_t size_t (*)(union u U) (toutes les valeurs de l'union peuvent en quelque sorte être insérées dans size_t et l'union est suffisamment petite pour que le passage par la valeur soit plus efficace), mais même avec cela, les appels de fonction ont un surcoût non négligeable qui tend à être significativement plus important que celui d'un saut dans une table de sauts générée par un commutateur.

Essayez quelque chose comme :

#include <stddef.h>
enum en { e_a, e_b, e_c };
union u{
   char a;
   int b;
   size_t c;
};

size_t u_get_a(union u U) { return U.a; }
size_t u_get_b(union u U) { return U.b; }
size_t u_get_c(union u U) { return U.c; }

struct fntag{ size_t (*u_get)(union u U); union u u_val; };
struct entag{ enum en u_type; union u u_val; };

struct fntag fntagged1000[1000]; struct entag entagged1000[1000];

void init(void) {
    for (size_t i=0; i<1000; i++)
        switch(i%3){
        break;case 0: 
            fntagged1000[i].u_val.a = i, fntagged1000[i].u_get = &u_get_a;
            entagged1000[i].u_val.a = i, entagged1000[i].u_type = e_a;
        break;case 1: 
            fntagged1000[i].u_val.b = i, fntagged1000[i].u_get = &u_get_b;
            entagged1000[i].u_val.b = i, entagged1000[i].u_type = e_b;
        break;case 2:
            fntagged1000[i].u_val.c = i, fntagged1000[i].u_get = &u_get_c;
            entagged1000[i].u_val.c = i, entagged1000[i].u_type = e_c;
        }
}

size_t get1000fromEnTagged(void)
{
    size_t r = 0;
    for(int i=0; i<1000; i++){
        switch(entagged1000[i].u_type){
        break;case e_a: r+=entagged1000[i].u_val.a;
        break;case e_b: r+=entagged1000[i].u_val.b;
        break;case e_c: r+=entagged1000[i].u_val.c;
        /*break;default: __builtin_unreachable();*/
        }
    }
    return r;
}

size_t get1000fromFnTagged(void)
{
    size_t r = 0;
    for(int i=0; i<1000; i++) r += (*fntagged1000[i].u_get)(fntagged1000[i].u_val);
    return r;
}

int main(int C, char **V)
{
    size_t volatile r;
    init();
    if(!V[1]) for (int i=0; i<1000000; i++) r=get1000fromEnTagged();
    else for (int i=0; i<1000000; i++) r=get1000fromFnTagged();

}

À -O2, j'obtiens plus de deux fois les performances du code basé sur les commutateurs.

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