LTO,虚拟化和虚拟表

时间:2011-08-12 21:54:11

标签: c++ c compiler-optimization

比较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("%d\n", obj.vtab->f());
}

我的GCC不会内联f,虽然它是直接调用的,即,是虚拟化的。 C ++中的等价物,

class A
{
public:
    virtual int f() = 0;
};

class B
{
public:
    int f() {return 5;}
};

int main()
{
    B b;
    printf("%d\n", b.f());
}

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

Update 2:为了在 C 中进行虚拟化,编译器必须证明虚拟表中的函数指针具有特定值。为了在 C ++ 中进行虚拟化,编译器必须证明该对象是特定类的实例。在第一种情况下,证据似乎更难。但是,虚拟表通常只在很少的地方进行修改,最重要的是:只是因为它看起来更难,并不意味着编译器不是那么好(否则你可能会认为xoring通常比添加两个更快)整数)。

4 个答案:

答案 0 :(得分:9)

不同之处在于,在C ++中,编译器可以保证虚拟表地址永远不会改变。在C中,它只是另一个指针,你可能会对它造成任何破坏。

  

但是,虚拟表通常只在极少数地方修改

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

答案 1 :(得分:4)

我试图在http://hubicka.blogspot.ca/2014/01/devirtualization-in-c-part-2-low-level.html中总结为什么泛型优化很难进行虚拟化。使用GCC 4.8.1为我的测试用例内联,但是在稍微不那么简单的测试用例中,你将指针传递给main中的“对象”,它不会。

原因是为了证明obj中的虚拟表指针和虚拟表本身没有改变,别名分析模块必须跟踪你可以指向它的所有可能的地方。在一个非平凡的代码中,你将事物传递到当前编译单元之外,这通常是一个迷失的游戏。

C ++为您提供有关何时可以更改对象类型以及何时知道对象的更多信息。 GCC利用它,它将在下一个版本中更多地使用它。 (我也会很快写下来的。)

答案 2 :(得分:3)

是的,如果编译器可以推断出虚拟化类型的确切类型,它可以“虚拟化”(甚至内联!)调用。编译器只有在能够保证无论如何才能保证这是必需的功能时才能这样做 主要关注点基本上是线程化。在C ++示例中,即使在线程环境中也能保证。在C中,无法保证,因为该对象可能被另一个线程/进程抓取,并被覆盖(故意或其他方式),因此该函数永远不会“devirtualized”或直接调用。在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.
}

答案 3 :(得分:1)

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

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