C++ 继承了 C 语言的函数指针,大多数情况下,我们使用 C 函数指针实现回调。在 C++ 中,另有成员函数指针:
struct A {
int foo(int);
};
int bar(A*, int);
int (A::*pmf)(int) = &A::foo;
int (*pf)(A*,int) = &bar;
int call_pmf(A* a, int x) { return (a->*pmf)(x); }
int call_pf(A* a, int x) { return pf(a, x); }
在 GCC 中,成员函数指针实际上由两个指针构成,实现如下:
struct MemFuncPtr {
intptr_t func_ptr;
intptr_t adjust_this_offset;
};
其中 adjust_this_offset 用来在多继承中调整 this 指针,如果没有多继承,或者该 class 为多继承中的第一个class,adjust_this_offset 就为 0。
同时,func_ptr 的最低位用来标识该成员函数指针指向的是否虚函数,如果是虚函数,func_ptr 表示该虚函数在虚表中的(偏移+1)。
因为函数地址都是对齐的,所以有效的函数地址的最低位永远为 0,所以将这个最低位作为是否虚函数的 flag不会有任何歧义。
由此可见,相比普通函数指针,通过成员函数指针调用成员函数,需要一些额外操作,例如以上 call_pmf 函数编译为:
call_pmf(A*, int):
movq pmf(%rip), %rax
addq pmf+8(%rip), %rdi
testb $1, %al
je .L39
movq (%rdi), %rdx
movq -1(%rdx,%rax), %rax
.L39:
jmp *%rax而调用普通函数的 call_pf 编译为:
call_pf(A*, int):
jmp *pf(%rip)由此可见,通过成员函数指针的调用代价远高于常规函数指针,但是,大家对性能的追求是永无止境的,为了解决这个问题,GCC 提供了一个扩展 Feature:Bound member functions。因为在很多 ABI 中,成员函数的调用规范和常规函数是一致的,例如上面 int A::foo(int)和int bar(A*, int),它们的调用规范是相同的,成员函数的隐式 this 指针是作为常规函数的第一个参数传递的。基于 ABI 的这个特性,GCC 可以将成员函数指针转化为普通函数指针:
typedef int (*pf_t)(A*, int);
pf_t pf = (pf_t)(&A::foo); // GCC 特有,非 C++ 标准
对于虚函数,可以在绑定时就拿到实际的函数地址:
struct B : A {
virtual ~B();
virtual int vfoo(int) = 0;
};
typedef int (A::*pmf_t)(int);
pf_t get_vpmf(B* b) { return (pf_t)(pmf_t)(b->*(&B::vfoo)); }
会被编译为:
get_vpmf(B*):
movq (%rdi), %rax
movq 16(%rax), %rax
ret这里 get_vpmf 囊括了几乎最复杂的情形:先将有虚函数的 B 的对象的指向虚函数的成员函数指针转化为没有虚函数的基类 A 的成员函数指针,再将其转化为(提取其中的)常规函数指针。
实际应用
在 ToplingDB 的底层算法库 topling-zip 的 BlobStore 中,读取单条数据函数,会根据各种不同的条件有所不同,如果在每次读取中都判断这些条件,会浪费 CPU 时间,所以在加载 BlobStore 对象时,根据生存期内的常量条件,生成不同的(成员)函数,通过(成员)函数指针来调用。这样就自然而然地遇到了这个问题,使用 GCC 的这个 Bound member functions 特性,提前将成员函数绑定到常规函数指针,避免了成员函数指针调用的开销。
其二,在 ToplingZipTable Iterator 中,需要调用 COIndex Iterator 的 Next/Prev,而 COIndex 中的 UintIndex, FixedLenIndex 的 Next/Prev 是非常 trivial 的,而虚函数调用本身需要额外两次内存访问(获取vtab指针,从vtab指针获取 Next/Prev 地址),这就导致虚函数调用本身的相对开销较高,并且 ToplingZipTable 的 Next/Prev 中是在一开始就调用 COIndex 的 Next/Prev 的,所以也无法通过其它语句利用 CPU 的指令级并行能力(pipeline、多发射...)来隐藏这两次内存(CPU Cache)访问的开销。所以,我们也通过 Bound member functions 特性,在创建 COIndex Iterator 时,将其虚函数 Next/Prev 提前绑定,这样提高那么一丢丢性能。
msvc & clang
msvc 隔绝于 gnu 界,其成员函数指针与普通函数指针相同,使用另外的机制实现虚函数、多继承等成员函数的绑定。
clang 兼容几乎所有 gcc 特性,但遗憾的是,clang 还没有实现 gcc 的这个特性,我刚给报了个 issue,希望 clang 可以早点实现这个特性。
MyTopling 已上线阿里云计算巢,欢迎体验
目前,基于 ToplingDB 的 MyTopling 成功上线计算巢,是 MySQL 的不二代替品,具体包括:私有化部署版,高级版,基础版,集成版(集成 LNMP/Wordpress),特价版 2核2G ¥99包年(企业用户 2核4G¥199包年)
MyTopling 支持免费试用,所有费用全免,包括 ECS 费用:
◆ 免费试用 MyTopling 集成版(集成 LNMP/Wordpress)
【完】




