c - 标记 union 中的函数指针为 "tag"

标签 c unions

我倾向于避免使用标记 union 的原因之一是,我不喜欢标记的 switch/case 语句可能会带来性能损失的想法,如果标签大于 4 个左右。

我的想法是,可以设置一个指向函数的指针来读取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() 进行“重载”,从而将其转换并写入不同的类型输出。

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

我的问题是:你见过在什么地方使用过这种技术吗? (我没有,而且我不知道这是否是因为现实世界的应用程序需要在 readval 函数上使用 _Generic() ,这需要 C11,或者是否是因为我的一些问题”我现在没有注意到)。您认为在当前的 Intel CPU 中,需要有多少个标签才能使指针的功能比 switch/case 更快?

最佳答案

你可以这么做。在您的情况下,更优化友好的函数签名将是 size_t size_t (*)(union u U) (所有 union 值都可以适合 size_t 和 union 足够小,可以更有效地传递值),但即使如此,函数调用也具有不可忽略的开销,该开销往往比通过 switch 生成的跳转表进行跳转要大得多。

尝试如下:

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

关于c - 标记 union 中的函数指针为 "tag",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65998964/

相关文章:

c++ - 在哪种情况下,我们在 C 中只定义枚举中的一个成员?

c - C 中的 vector 结构

c - 如何将混合类型数组转换为 char 数组并返回?

包含结构的 C union - 内存映射 - 编译器跳过一个字节?

崩溃程序不生成核心转储

c - Mpi函数定义

c - C中的"Classes and initializers"

c - union 和结构打包问题

从动态分配的文件中计算最多重复出现的元音

c - 在 C 中使用 union 的 switch 语句给我带来了问题,我的代码出了什么问题?