Can virtual functions be inlined [duplicate]
Asked Answered
E

2

9

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

Eulaliaeulaliah answered 25/8, 2013 at 18:4 Comment(3)
If you think about it, inlining virtual functions doesn't really make sense. The only case I can see is if you know the type at compile time, but even then I'm not sure a compiler would do the optimization.Afreet
#734237Quacksalver
@Borgleader: they do, when they can. However no compiler is really good at it because of complex rules in the C++ language regarding the construction and destruction of polymorphic objects. Furthermore, since there is no JITing in general, the very subset of situations where it can be done is limited.Colet
J
14

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 example:

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.

Jenine answered 25/8, 2013 at 18:6 Comment(9)
A non-virtual call to a virtual function (a->A::func()) is another somewhat obvious example where inlining generally works.Putrescent
I view the link @Quacksalver gave, it seems that inlined virtual destructor makes sense, but I'm still a little confused about how destructors are inlinedEulaliaeulaliah
when the compiler can prove that the static type matches the dynamic type: it's actually more complicated than that. Consider Base* b = new Derived{}; b->func();, here the call can be inlined if the compiler is smart enough to realized that the dynamic type of b is necessarily Derived. Clang is such a smart compiler.Colet
@MatthieuM.: Yes, you are right. I will rephrase the answer in this respect.Disinterest
@Ghostblade: Destructors aren't special with respect to inlining: If the compiler can determine which destructor needs to be called, it can be inlined. That said, destructors are unusual with respect to virtual functions as a destructor will be called even when a derived class overrides the destructor! The virtual dispatch for a destructor is only only used to determine which is the most derived destructor and, thus, needs to be called first.Disinterest
@Dietmar Kühl: You mean only the call of the lowest derived destructor is a virtual dispatch so as to determine the type, and the calls to base destructors can be inlined? but this happens at runtimeEulaliaeulaliah
@Matthieu M.: So Clang can know RTTI at compiling time, amazingEulaliaeulaliah
@Ghostblade: it's more a game of tracking down the source of the value and see if it can be resolved into a concrete type :) Still, it seems like a pity to do that kind of tracking knowing that LLVM will do it too :/Colet
@Ghostblade: No. What I'm saying is that the correct destructor to be called can also be determined at compile time under some conditions. The destructor isn't special in this sense. When the correct destructor is determine at compile time, it can be inline. The other part was just describing what is special about virtual dispatch for destructors but it doesn't have any impact on whether the destructor can be inline.Disinterest
C
5

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.

Colet answered 25/8, 2013 at 18:44 Comment(1)
+1 for the reference to the code.Rhee

© 2022 - 2024 — McMap. All rights reserved.