Can virtual functions be inlined [duplicate]

2019-03-26 06:33发布

问题:

This question already has an answer here:

  • Are inline virtual functions really a non-sense? 12 answers
  • inline virtual function 3 answers

If I define a class like this:

class A{
public:
    A(){}
    virtual ~A(){}
    virtual void func(){}
};

Does it mean that that the virtual destructor and func are inlined

回答1:

Whether the compiler chooses to inline a function which is defined inline is entirely up to the compiler. In general, virtual functions can only be inlined when the compiler can either prove that the static type matches the dynamic type or when the compiler can safely determine the dynamic type. For example, when you use a value of type A the compiler knows that the dynamic type cannot be different and it can inline the function. When using a pointer or a reference the compiler generally cannot prove that the static type is the same and virtual functions generally need to follow the usual virtual dispatch. However, even when a pointer is used, the compiler may have enough information from the context to know the exact dynamic type. For example, MatthieuM. gave the following exmaple:

A* a = new B;
a->func();

In this case the compiler can determine that a points to a B object and, thus, call the correct version of func() without dynamic dispatch. Without the need for the dynamic dispatch, func() could then be inlined. Of course, whether compilers do the corresponding analysis depends on its respective implementation.

As hvd correctly pointed out, the virtual dispatch can be circumvented by calling a virtual function will full qualification, e.g., a->A::func(), in which case the virtual function can also be inlined. The main reason virtual functions are generally not inlined is the need to do a virtual dispatch. With the full qualification the function to be called is, however, known.



回答2:

Yes, and in multiple ways. You can see some examples of devirtualization in this email I sent to the Clang mailing list about 2 years ago.

Like all optimizations, this is pending the compiler abilities to eliminate alternatives: if it can prove that the virtual call is always resolved in Derived::func then it can call it directly.

There are various situations, let us start first with the semantic evidences:

  • SomeDerived& d where SomeDerived is final allows to devirtualization of all method calls
  • SomeDerived& d, d.foo() where foo is final also allows devirtualization of this particular call

Then, there are situations where you know the dynamic type of the object:

  • SomeDerived d; => the dynamic type of d is necessarily SomeDerived
  • SomeDerived d; Base& b; => the dynamic type of b is necessarily SomeDerived

Those 4 devirtualization situations are usually solved by the compiler front-end because they require fundamental knowledge about the language semantics. I can attest that all 4 are implemented in Clang, and I would think they are also implemented in gcc.

However, there are plenty of situations where this breaks down:

struct Base { virtual void foo() = 0; };
struct Derived: Base { virtual void foo() { std::cout << "Hello, World!\n"; };

void opaque(Base& b);
void print(Base& b) { b.foo(); }

int main() {
    Derived d;

    opaque(d);

    print(d);
}

Even though here it is obvious that the call to foo is resolved to Derived::foo, Clang/LLVM will not optimize it. The issue is that:

  • Clang (front-end) does not perform inlining, thus it cannot replace print(d) by d.foo() and devirtualize the call
  • LLVM (back-end) does not know the semantics of the language, thus even after replacing print(d) by d.foo() it assumes that the virtual pointer of d could have been changed by opaque (whose definition is opaque, as the name implies)

I've followed efforts on the Clang and LLVM mailing list as both sets of developers reasoned about the loss of information and how to get Clang to tell LLVM: "it's okay" but unfortunately the issue is non-trivial and has not been solved yet... thus the half-assed devirtualization in the front-end to try and get all obvious cases, and some not so obvious ones (even though, by convention, the front-end is not where you implement them).


For reference, the code for the devirtualization in Clang can be found in CGExprCXX.cpp in a function called canDevirtualizeMemberFunctionCalls. It's only ~64 lines long (right now) and thoroughly commented.