LTO、去虚拟化和虚拟表

LTO, Devirtualization, and Virtual Tables

本文关键字:虚拟 虚拟化 LTO      更新时间:2023-10-16

比较c++中的虚函数和C中的虚表,总的来说(对于足够大的项目),编译器在去虚拟化方面做得一样好吗?

简单地说,c++中的虚函数似乎有更多的语义,因此可能更容易反虚拟化。

更新: Mooing Duck提到内联非虚拟化函数。快速检查虚拟表的优化:

struct vtab {
    int (*f)();
};
struct obj {
    struct vtab *vtab;
    int data;
};
int f()
{
    return 5;
}
int main()
{
    struct vtab vtab = {f};
    struct obj obj = {&vtab, 10};
    printf("%dn", obj.vtab->f());
}

我的GCC不会内联f,尽管它是直接调用的,即de虚拟化。c++中的等效符

class A
{
public:
    virtual int f() = 0;
};
class B
{
public:
    int f() {return 5;}
};
int main()
{
    B b;
    printf("%dn", b.f());
}

甚至内联f。所以这是C和c++之间的第一个区别,尽管我不认为c++版本中添加的语义在这种情况下是相关的。

更新2:为了在C中反虚拟化,编译器必须证明虚表中的函数指针具有一定的值。为了在c++ 中去虚拟化,编译器必须证明对象是特定类的实例。在第一种情况下,证明似乎更难。然而,虚拟表通常只在很少的地方被修改,而且最重要的是:仅仅因为它看起来更难,并不意味着编译器在这方面不够好(否则您可能会争辩说xoring通常比两个整数相加要快)。

不同的是,在c++中,编译器可以保证虚表地址永远不会改变。在C语言中,它只是另一个指针,你可以用它造成任何形式的破坏。

然而,虚拟表通常只在很少的地方被修改

编译器不知道C中的。在c++中,它可以假设永远不会改变。

我试图在http://hubicka.blogspot.ca/2014/01/devirtualization-in-c-part-2-low-level.html中总结为什么通用优化很难去虚拟化。你的测试用例对我来说是与GCC 4.8.1内联的,但在稍微不那么琐碎的测试用例中,你将指针传递给main之外的"对象",它不会。

原因是为了证明obj中的虚表指针和虚表本身没有改变,别名分析模块必须跟踪所有可能指向它的位置。在不重要的代码中,如果您在当前编译单元之外传递东西,这通常是失败的。

c++提供了更多关于对象类型何时可能改变以及何时已知的信息。GCC使用了它,并且在下一个版本中会更多地使用它。

是的,如果编译器可以推断出虚拟类型的确切类型,它可以"去虚拟化"(甚至内联)调用。只有当编译器能够保证无论如何,这都是需要的函数时,它才能这样做。
主要的问题是线程。在c++示例中,即使在线程环境中,这些保证也是有效的。在C语言中,这是无法保证的,因为对象可能被另一个线程/进程抓取,并被覆盖(有意或无意),所以函数永远不会被"去虚拟化"或直接调用。在C语言中,查找总是在那里。

struct A {
    virtual void func() {std::cout << "A";};
}
struct B : A {
    virtual void func() {std::cout << "B";}
}
int main() {
    B b;
    b.func(); //this will inline in optimized builds.
}

这取决于您比较编译器内联的内容。与链接时间或配置文件引导或实时优化相比,编译器使用的信息更少。使用更少的信息,编译时优化将更加保守(并且总体上做更少的内联)。

编译器通常在内联虚函数方面仍然相当不错,因为它相当于内联函数指针调用(例如,当您将一个自由函数传递给STL算法函数时,如sortfor_each)。