标记联合中的函数指针为 "tag"
Function pointer as "tag" in tagged 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
指针,以便它指向一个函数,该函数读取您在联合中写入的最后一个字段。是的,有一个棘手的问题,就是return读取值的位置(因为同一个函数指针不能指向不同类型的函数return)。我选择通过指向 void
的指针 return 值,这样这样的函数也可以用 C11 _Generic()
“重载”,从而为输出转换和写入不同的类型。
当然,调用指向函数的指针会产生性能开销,我想检查 enum
的值要重得多,但在某些时候,如果标签的数量很大,我相信它会比 switch/case
.
快
我的问题是:你见过这种技术在什么地方使用过吗? (我没有,我不知道是不是因为在现实世界中应用它需要在 readval 函数上使用 _Generic()
,这需要 C11,或者是因为一些我没有注意到的问题片刻)。你猜你需要多少标签才能使指针在当前的英特尔 CPU 中比 switch/case
更快地运行?
你可以做到。在您的情况下,更优化友好的函数签名将是 size_t size_t (*)(union u U)
(所有联合值都可以放入 size_t
并且联合足够小,可以通过值传递更多高效),但即使如此,函数调用也有不可忽略的开销,它往往比通过开关生成的跳转的开销大得多 table.
试试这样的东西:
#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 时,我在基于开关的代码中获得了两倍多的性能。
我倾向于避免使用标记联合的原因之一是我不喜欢这样的想法,如果标记的数量更多,标记的 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
指针,以便它指向一个函数,该函数读取您在联合中写入的最后一个字段。是的,有一个棘手的问题,就是return读取值的位置(因为同一个函数指针不能指向不同类型的函数return)。我选择通过指向 void
的指针 return 值,这样这样的函数也可以用 C11 _Generic()
“重载”,从而为输出转换和写入不同的类型。
当然,调用指向函数的指针会产生性能开销,我想检查 enum
的值要重得多,但在某些时候,如果标签的数量很大,我相信它会比 switch/case
.
我的问题是:你见过这种技术在什么地方使用过吗? (我没有,我不知道是不是因为在现实世界中应用它需要在 readval 函数上使用 _Generic()
,这需要 C11,或者是因为一些我没有注意到的问题片刻)。你猜你需要多少标签才能使指针在当前的英特尔 CPU 中比 switch/case
更快地运行?
你可以做到。在您的情况下,更优化友好的函数签名将是 size_t size_t (*)(union u U)
(所有联合值都可以放入 size_t
并且联合足够小,可以通过值传递更多高效),但即使如此,函数调用也有不可忽略的开销,它往往比通过开关生成的跳转的开销大得多 table.
试试这样的东西:
#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 时,我在基于开关的代码中获得了两倍多的性能。