暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

深入理解成员函数指针

Topling 2024-06-27
191

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 提供了一个扩展 FeatureBound 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 基础版

 ◆ 免费试用 MyTopling 集成版(集成 LNMP/Wordpress)

【完】

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论