编译器何时可以静态绑定调用虚函数?

13

我原以为如果在编译时已知类的类型(例如,如果类实例未通过引用或指针使用,如下面的Case 1),那么编译器就能够静态解析对虚函数的函数调用。

然而,我注意到使用Visual Studio 2010的C++编译器存在奇怪的行为,并想知道是否有任何理由使编译器不能将对“正确”虚函数的调用静态绑定,即当具有虚函数的类的实例是由引用访问的结构体成员时。

我应该期望编译器在下面的Case 2中将调用静态绑定到f()吗? 即使a是A而不是A&,cr的“引用性”是否会以某种方式传播到cr.a?

struct A
{
    virtual void f() ;
    virtual ~A() ;
};

struct B : A
{
    virtual void f() ;
    virtual ~B() ;
};

struct C {
    A a ;
    B b ;
};

C & GetACRef() ;

void test()
{
    // Case 1) The following calls to f() are statically bound i.e.
    // f() is called without looking up the virtual function ptr.
    C c ;  
    c.a.f() ;
    c.b.f() ;
    A a ;
    a.f() ;

    // Case 2) The following calls to f() go through the dynamic dispatching
    // virtual function lookup code. You can check if you generate the .asm
    // for this file.
    C & cr = GetACRef() ; // Note that C is not polymorphic
    cr.a.f() ; // visual C++ 2010 generates call to f using virtual dispatching
    cr.b.f() ; // visual C++ 2010 generates call to f using virtual dispatching  
}

有趣的是,似乎Clang也没有优化虚函数调用。很有趣。(如果我将B::f标记为final,则它成功地优化了cr.b.f() - Matthieu M.
2个回答

3

显然编译器的编写者并没有费心解决这个问题。也许在真实代码中这种情况不够普遍,不值得他们的注意。

如果 GetACRef 在其他地方定义,那么 C 可能是多态的,这可能会影响优化。

请注意,编译器并不会解决每一个对人类来说“显然”的小测试程序的可能情况。编译器的重点是出现在大型实际程序中的情况。


“GetACref”的定义的一个使得这个优化无效的例子是什么? - user168715
1
“GetACRef” 可以返回从 C 派生的 DE 的引用(在另一个模块中)。如果编译器无法证明这 永远不会 影响代码,则无法进行优化。 - Bo Persson
1
即使如此,cr.a 也必须引用 C::a,对吗?或者我漏掉了什么? - user168715
在这个特定的情况下,是的。在一般情况下,谁知道呢? - Bo Persson
cr.a怎么可能不是指C中的'a'呢?如果'a'是一个虚函数,我可以理解它会经过一些虚分派,但'a'只是一个成员。有没有例子能说明.a可能是其他东西?谢谢! - Carlos
1
@Carlos - 编译器编写者试图解决一般情况,而不是特定的测试用例。也许“返回对具有未被多态使用的虚函数的成员对象的引用的函数”还没有达到他们的待办事项列表的顶部? - Bo Persson

2
我不知道为什么MSVC不能将你的“Case 2”场景编译成直接调用-这是完全可能的。只有微软才能回答这个问题。
请注意,GCC确实执行您正在寻找的优化(使用MinGW 4.5.1和-O2进行测试)。
此外,即使在以下序列中,MSVC也使用vtable分派(为了清晰起见-我使用了/Ox优化选项):
A a;
A& ar(a);
ar.f();

因此,没有必要添加函数或容器结构来增加编译器的潜在混淆 - 在该序列中,编译器没有理由不能将ar.f()a.f()完全相同地处理。但正如Bo Persson所建议的那样,也许这不是一个非常常见的优化场景(或者MS根本没有解决它)。再次强调,只有MS的编译器开发人员才能回答。

我不确定我是否应该将这种行为归类为“奇怪” - 这只是错过了一次优化机会而已。我不确定这种情况有多常见。在这种情况下,您是否应该期望编译器生成静态绑定的调用?也许。但我认为它不会太令人惊讶,它并没有发生。

也许应该在MS Connect上开启一个问题。


1
你的例子实际上更加复杂。在 cr.a.f() 中,cr.a 的类型必须是 A,因此编译器可以确定动态类型。而要知道 A& 绑定到了什么,则需要控制流分析,这更加耗费资源且难以实现。 - Matthieu M.
有趣的是,我用Clang 2.9进行了测试,在你的情况下它成功地分析出A&绑定到了A并且去虚拟化调用,尽管在OP的情况下仍然失败。我会尝试使用更新的版本,这似乎是一个足够简单的优化案例(在我的高水平上:p)。 - Matthieu M.
@Matthieu:你说得对,这需要一种不同类型的分析。我没有考虑过这一点。请注意,GCC将ar.f()解析为静态调用。这两种情况都是我认为编译器通常执行的优化;但是,我不确定在现实世界中它有多重要。 - Michael Burr
这似乎不太常见 :) 不过我不会太担心,编译器已经在优化虚函数调用和静态函数调用方面做了很多工作,开销最多不应超过15%...所以除非函数非常简单,否则这可能是微不足道的。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接