函数指针作为标记联合中的“标记”

问题描述

我倾向于避免使用标记联合的原因之一是我不喜欢如果标记数量大于 4,标记switch/case 语句可能会引入的性能损失的想法大约。

我只是有一个想法,可以不使用标签,而是设置一个指向函数的指针,该函数读取 union 中最后写入的值。例如:

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

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

然后,每当您将值写入 val 时,您也会相应地更新 readval 指针,使其指向一个函数,该函数读取您在联合中写入的最后一个字段。是的,有一个难题,就是返回读取值的位置(因为相同的函数指针不能指向返回不同类型的函数)。我选择通过指向 void 的指针返回值,以便这样的函数也可以用 C11 _Generic() “重载”,从而转换和写入不同类型的输出

当然,调用一个函数的指针有性能开销,我猜比检查 enum 的值要重得多,但在某些时候,如果标签数量很大,我相信它会比 switch/case 快。

我的问题是:你有没有见过这种技术在某处使用过? (我没有,我不知道是不是因为现实世界中的这个应用需要在 readval 函数上使用 _Generic(),这需要 C11,或者是因为我没有注意到的一些问题片刻)。您猜您需要多少个标签才能使指向函数的指针比 switch/case 更快(在当前的 Intel cpu 中)?

解决方法

你可以这样做。在您的情况下,更优化友好的函数签名将是 size_t size_t (*)(union u U) (所有联合值都可以适合 size_t 并且联合足够小,可以通过值传递更有效),但是即便如此,函数调用的开销也不容忽视,而且往往比通过开关生成的跳转表跳转的开销大得多。

尝试类似:

#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 时,我在基于开关的代码中获得了两倍以上的性能。