C语言 虚拟函数与函数指针-性能?

abithluo  于 2023-05-28  发布在  其他
关注(0)|答案(6)|浏览(187)

在多态基类上调用C++虚函数和调用C风格的函数指针一样快吗?真的有什么区别吗?
我正在考虑重构一些注重性能的代码,这些代码利用了函数指针,并将它们改为多态中的虚函数。

628mspwn

628mspwn1#

我会说大多数C++实现的工作方式与此类似(可能是第一个编译成C的实现,产生了这样的代码):

struct ClassVTABLE {
    void (* virtuamethod1)(Class *this);
    void (* virtuamethod2)(Class *this, int arg);
};

struct Class {
    ClassVTABLE *vtable;
};

然后,给定一个示例Class x,为它调用方法virtualmethod1就像x.vtable->virtualmethod1(&x),因此需要一个额外的解引用,一个从vtable索引的查找,以及一个额外的参数(= this)被推送到堆栈/传递到寄存器。
然而,编译器可能可以优化函数中的示例上的重复方法调用:由于示例Class x在构造后不能更改其类,因此编译器可以将整个x.vtable->virtualmethod1视为公共子表达式,并将其移出循环。因此,在这种情况下,在单个函数内重复的虚方法调用在速度上等同于通过简单的函数指针调用函数。

6rqinv9w

6rqinv9w2#

在多态基类上调用C++虚函数和调用C风格的函数指针一样快吗?真的有什么区别吗?
苹果和橘子。在一个极小的“一对一种级别,虚函数调用涉及的工作稍微多一些,因为从vptrvtable条目需要间接/索引开销。

但是虚函数调用可以更快

好吧,怎么会这样?我只是说虚函数调用需要稍微多做一些工作,这是真的。
人们往往忘记的是,试图在这里做一个更密切的比较(试图使它少一点苹果和橘子,即使它是苹果和橘子)。我们通常不会创建一个只有一个虚函数的类。如果我们这样做了,那么性能(甚至代码大小)肯定会有利于函数指针。我们经常有这样的事情:

class Foo
{
public:
    virtual ~Foo() {}
    virtual f1() = 0;
    virtual f2() = 0;
    virtual f3() = 0;
    virtual f4() = 0;
};

...在这种情况下,更“直接”的函数指针类比可能是这样的:

struct Bar
{
     void (*f1)();
     void (*f2)();
     void (*f3)();
     void (*f4)();
};

在这种情况下,在Foo的每个示例中调用虚函数可以比Bar高效得多。这是因为Foo只需要将单个vptr存储到一个被重复访问的中央vtable中。这样,我们就可以改进引用的局部性(更小的Foos和那些可能更好地适应缓存行的数量,更频繁地访问Foo's中央vtable)。
另一方面,Bar需要更多的内存,并且在Bar的每个示例中有效地复制Foo's vtable的内容(假设有一百万个FooBar的示例)。在这种情况下,增加Bar大小的冗余数据量通常会大大超过每次函数指针调用所做的工作量。
如果我们只需要为每个对象存储一个函数指针,而这是一个极端的热点,那么只存储一个函数指针可能会更好(例如:对于那些远程实现类似std::function的东西的人来说,只存储一个函数指针可能是有用的)。
所以这是一种苹果和橘子,但是如果我们正在建模一个用例,那么存储中央共享函数地址表(在C或C++中)的vtable方法可能会更有效。
如果我们在建模一个用例,在这个用例中,我们只有一个函数指针存储在一个对象中,而如果一个vtable中只有一个虚函数,那么函数指针将稍微更有效。

js4nwp54

js4nwp543#

你不太可能看到很大的差异,但就像所有这些事情一样,通常是小细节(例如编译器需要将this指针传递给虚函数)会导致性能差异。virtual函数本身是一个函数指针,所以一旦编译器完成了它的工作,在这两种情况下您可能会得到非常相似的代码。
这听起来像是对虚函数的一个很好的使用,如果有人反对并说“会有性能差异”,我会说“证明它”。但是如果你不想讨论这个问题,那就做一个基准测试(如果还没有的话)来衡量现有代码的性能,重构它(或它的一部分)并比较结果。理想情况下,在几台不同的机器上进行测试,这样你就不会得到在你的机器上工作得更好的结果,但在其他类型的机器上(不同代的处理器,不同的制造商或处理器等)就不那么好了。

fykwrbwg

fykwrbwg4#

一个虚函数调用涉及两个解引用,其中一个是索引的,即比如*(object->_vtable[3])()
通过函数指针的调用涉及一次解引用。
方法调用还需要传递一个隐藏参数,以this的形式接收。
除非方法体实际上是空的,并且没有参数或返回值,否则您不太可能注意到差异。

xmq68pz9

xmq68pz95#

函数指针调用和虚函数调用之间的区别可以忽略不计,除非你已经测量到上面是一个瓶颈。
唯一的区别是:

  • 虚函数具有对vtable的内存读取,以及对函数地址的间接调用
  • 一个函数指针只有一个对函数的间接调用

这是因为虚函数需要查找它将要调用的函数的地址,而函数指针已经知道它(因为它存储在它自己中)。
我想补充一点,既然你是在使用C++,虚方法应该是一条路。

fumotvh3

fumotvh36#

Speed test results

#include <iostream>
#include <vector>

#define CALCULATING return 1;
//#define CALCULATING int res = 0;\
                    for (int i = 0; i < 10000; i++)\
                        res += i;\
                    return res;

static int Deriver1_foo_pf_impl()
{
    CALCULATING
}

int f_foo()
{
    CALCULATING
}

inline int if_foo()
{
    CALCULATING
}

class MyClass
{
public:
    int foo()
    {
        CALCULATING
    }
};

class IBase
{
public:
    virtual int foo()
    {
        CALCULATING
    }
};

class Deriver1 : public IBase
{
public:
    int foo() override
    {
        CALCULATING
    }
};

class Deriver2 : public Deriver1
{
public:
    int foo() override
    {
        CALCULATING
    }
};

class IBase_pf
{
public:
    int (*foo)();
};

class Deriver1_pf : public IBase_pf
{
protected:

public:
    Deriver1_pf()
    {
        this->foo = &f_foo;
    }
};

int main()
{
#ifdef _DEBUG
    std::cout << "DEBUG VERSION" << std::endl << std::endl;
#else
    std::cout << "RELEASE VERSION" << std::endl << std::endl;
#endif // _DEBUG

    std::cout.precision(10);

    unsigned long long countOfCalls = 1000000000;
    unsigned long long trash = 0;

    int (*pf_variable)() = &f_foo;

    Deriver2 vpt;
    Deriver1_pf fpo;
    MyClass mcf;

    clock_t start;
    clock_t end;

    std::cout << "Count of call funtions and methods : " << countOfCalls << std::endl << std::endl;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += vpt.foo();
    }
    end = clock();
    
    std::cout << "Virtual method : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();

    for (int i = 0; i < countOfCalls; i++)
    {
        trash += fpo.foo();
    }
    end = clock();

    std::cout << "Function poiter from base overridden in deriver : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();

    for (int i = 0; i < countOfCalls; i++)
    {
        trash += mcf.foo();
    }
    end = clock();

    std::cout << "Classic method : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += pf_variable();
    }
    end = clock();

    std::cout << "Call classic c-style function by function pointer (Callback) : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += f_foo();
    }
    end = clock();

    std::cout << "Classic C-style function : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += if_foo();
    }
    end = clock();

    std::cout << "Inline classic C-style function : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    return 0;
}

Virtaul方法比回调更快。它可能是虚拟方法优化的结果,也可能是回调缓存丢失的结果,或者两者都有

相关问题