类成员函数的函数指针与任意函数的指针之间的区别

问题描述

我正在尝试测试最快的方法调用函数指针以绕过有限数量的参数的模板。我写了这个基准测试:https://gcc.godbolt.org/z/T1qzTd

我注意到指向类成员函数函数指针增加了很多我难以理解的开销。我的意思是:

具有如下定义的结构条和函数foo:

body

一个选项(在Godbolt代码GET中)通过索引指向定义为的类成员函数函数指针数组来调用模板化函数

template<uint64_t r>
struct bar {
    template<uint64_t n>
    uint64_t __attribute__((noinline))
    foo() {
        return r * n;
    }
    
    // ... function pointers with pointers to versions of foo below

但是,此组件似乎有很多绒毛:

#define DO_DIRECT

我在理解上有困难。

相反, /* all of this inside of struct bar */ typedef uint64_t (bar::*foo_wrapper_direct)(); const foo_wrapper_direct call_foo_direct[NUM_FUNCS] = { &bar::foo<0>,// a bunch more function pointers to templated foo... }; // to call templated foo for non compile time input uint64_t __attribute__((noinline)) foo_direct(uint64_t v) { return (this->*call_foo_direct[v])(); } 方法定义为:

bar<9ul>::foo_direct(unsigned long):
        salq    $4,%rsi
        movq    264(%rsi,%rdi),%r8
        movq    256(%rsi,%rax
        addq    %rdi,%r8
        testb   $1,%al
        je      .L96
        movq    (%r8),%rdx
        movq    -1(%rdx,%rax),%rax
.L96:
        movq    %r8,%rdi
        jmp     *%rax

具有一些非常简单的汇编:

#define DO_INDIRECT

我试图理解为什么直接使用指向类成员函数函数指针的// forward declare bar and call_foo_wrapper template<uint64_t r> struct bar; template<uint64_t r,uint64_t n> uint64_t call_foo_wrapper(bar<r> * b); /* inside of struct bar */ typedef uint64_t (*foo_wrapper_indirect)(bar<r> *); const foo_wrapper_indirect call_foo_indirect[NUM_FUNCS] = { &call_foo_wrapper<r,0> // a lot more templated versions of foo ... }; uint64_t __attribute__((noinline)) foo_indirect(uint64_t v) { return call_foo_indirect[v](this); } /* no longer inside struct bar */ template<uint64_t r,uint64_t n> uint64_t call_foo_wrapper(bar<r> * b) { return b->template foo<n>(); } 方法具有如此多的绒毛,以及如何(如果可能的话)对其进行更改以消除绒毛。

注意:我使用bar<9ul>::foo_indirect(unsigned long): jmp *(%rdi,%rsi,8) 只是为了简化检查程序集。

谢谢。

p.s,如果有更好的方法可以将运行时参数转换为模板参数,我希望在示例/联机帮助页中找到一个链接

解决方法

C ++成员函数指针必须能够指向非虚拟函数或虚拟函数。在典型的vtable / vptr实现中,调用虚拟函数涉及从对象表达式中的vptr查找正确的代码地址,并可能将偏移量应用于对象参数地址。

g ++使用Itanium ABI,因此foo_direct的程序集正在按section 2.3中所述解释所访问的指针到成员函数的值。如果函数是虚拟的,它将通过对象表达式的vptr查找代码地址;如果不是虚拟的,则仅从指针到成员的值中复制代码地址。

我认为,如果优化可以看到类类型没有虚拟函数并且为final,则可能跳过虚拟函数调用逻辑。不过,我不知道g ++或其他编译器是否进行了任何此类优化。