16

Comparing virtual functions in C++ and virtual tables in C, do compilers in general (and for sufficiently large projects) do as good a job at devirtualization?

Naively, it seems like virtual functions in C++ have slightly more semantics, thus may be easier to devirtualize.

Update: Mooing Duck mentioned inlining devirtualized functions. A quick check shows missed optimizations with virtual tables:

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());
}

My GCC will not inline f, although it is called directly, i.e., devirtualized. The equivalent in C++,

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

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

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

does even inline f. So there's a first difference between C and C++, although I don't think that the added semantics in the C++ version are relevant in this case.

Update 2: In order to devirtualize in C, the compiler has to prove that the function pointer in the virtual table has a certain value. In order to devirtualize in C++, the compiler has to prove that the object is an instance of a particular class. It would seem that the proof is harder in the first case. However, virtual tables are typically modified in only very few places, and most importantly: just because it looks harder, doesn't mean that compilers aren't as good in it (for otherwise you might argue that xoring is generally faster than adding two integers).

ccom
  • 351
  • 1
  • 3
  • 8
  • just to clarify, are you asking if function pointers whose values are "known" at compile time are reduced to direct calls in with a good optimizing C compiler? – Evan Teran Aug 12 '11 at 22:06
  • Yes. In particular in real code, where it may be difficult for the compiler to deduce what it knows. – ccom Aug 12 '11 at 22:11
  • 1
    Who even cares. Correctness is king as long as the code works why do you care about such a small macro optimization. The benefits of this kind of things are always going to be swamped by other factors that you can't even think about. – Martin York Aug 12 '11 at 22:33
  • 7
    @Martin: In some cases, it's just incorrect what you wrote. Moreover, why should a more detailed insight into compilers and the used languages be without benefit? – ccom Aug 13 '11 at 00:01
  • @ccom: Because you are not looking for insight into the language. You are looking for implementation details which are not relevant to the language user. Programmers are falable and often get things wrong. Compilers on the other hand will repeat the same task again and again and again without variation, thus this kind of optimizations is perfect fro the compiler to do but very prone to error when done by a programmer. You should not be concentrating on this kind of macro optimization that humans are terrible at anyway and consecrate on the things that compiler can't do (scalability/algorithms) – Martin York Aug 13 '11 at 16:00
  • @Martin: I was never mentioning doing anything by hand. Actually, I wouldn't even know how to safely devirtualize a member function in C++. My question is looking for insight insofar it concerns the importance of the mentioned bit of semantics. As Mooing Duck in his answer explains, this *subtlety* may have implications for threading. Moreover, it's about the current state of the art of optimization, and to what degree language constructs may have an impact on it. – ccom Aug 13 '11 at 17:07
  • I think an interesting additional test would be to add `const` to the C-style implementation, and then run it through a C++ optimizer. – Ben Voigt Jan 02 '13 at 13:23
  • In this C9 video, Eric Brummer explains how VC++ devirtualizes based on profile data (at 22:30). http://channel9.msdn.com/Events/GoingNative/2013/Compiler-Confidential in a nutshell, then it is the same for C and C++. –  Jan 29 '14 at 16:31
  • In your C++ code, `B` doesn't derive from `A`. Is this the code you used in your testing? – Drew Noakes Apr 28 '14 at 12:23

4 Answers4

9

The difference is that in C++, the compiler can guarantee that the virtual table address never changes. In C then it's just another pointer and you could wreak any kind of havoc with it.

However, virtual tables are typically modified in only very few places

The compiler doesn't know that in C. In C++, it can assume that it never changes.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • Right, that's why I am asking whether it *really* makes a difference to an optimizing compiler. After all, a compiler may infer that the virtual table never changes. – ccom Aug 13 '11 at 10:53
  • 3
    @ccom: The instant you pass a pointer or reference outside of that TU, you make it virtually impossible for the compiler to know that. That means in C, if you define the "member functions" somewhere else, you lose that guarantee, unless the compiler is very smart. In C, the compiler has to *prove* you never change it. In C++, the compiler can *assume* it never changes. – Puppy Aug 13 '11 at 12:15
4

I tried to summarize in http://hubicka.blogspot.ca/2014/01/devirtualization-in-c-part-2-low-level.html why generic optimizations have hard time to devirtualize. Your testcase gets inlined for me with GCC 4.8.1, but in slightly less trivial testcase where you pass pointer to your "object" out of main it will not.

The reason is that to prove that the virtual table pointer in obj and the virtual table itself did not change the alias analysis module has to track all possible places you can point to it. In a non-trivial code where you pass things outside of the current compilation unit this is often a lost game.

C++ gives you more information on when type of object may change and when it is known. GCC makes use of it and it will make a lot more use of it in the next release. (I will write on that soon, too).

Jan Hubička
  • 609
  • 6
  • 5
3

Yes, if it is possible for the compiler to deduce the exact type of a virtualized type, it can "devirtualize" (or even inline!) the call. A compiler can only do this if it can guarantee that no matter what, this is the function needed.
The major concern is basically threading. In the C++ example, the guarantees hold even in a threaded environment. In C, that can't be guaranteed, because the object could be grabbed by another thread/process, and overwritten (deliberately or otherwise), so the function is never "devirtualized" or called directly. In C the lookup will always be there.

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.
}
Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
  • The question is not *if* compilers can devirtualize, the question is if compilers can devirtualze *equally well*, given that a virtual table has less semantics. – ccom Aug 13 '11 at 00:03
  • Can C devirtualize at all? I didn't know that, and misread the question. – Mooing Duck Aug 13 '11 at 00:18
  • I updated the question with example where C devirtualizes, which, strictly speaking, is something like constant propagation. Conceptually, however, it is devirtualization. – ccom Aug 13 '11 at 10:18
  • 1
    How do you account for the fact that, in the C example, the function *is* devirtualized? – ccom Aug 13 '11 at 17:10
  • I think that question is a communications issue and not a technical one. I was thinking "devirtualization" was when the compiler guessed which vtab was being used, and had the caller call the virtual function directly, without the vtab lookup. What did you mean by "devirtualizing" a function? – Mooing Duck Aug 15 '11 at 15:43
  • `My GCC will not inline f, although it is called directly, i.e., devirtualized.` Even though f is called directly, it _isn't_ devirtualized, as the compiler can't guarantee where the function pointer will point at runtime. – Mooing Duck Aug 15 '11 at 15:52
  • 1
    I meant the same thing. The vtab is not looked up in the C example (with gcc -O2). – ccom Aug 15 '11 at 17:12
  • huh, I didn't think that C could do that. Good job gcc! All I'm left with then is that it's frequently trivial to show that a object is B (in my example main), whereas in C it has to show that the value hasn't changed. – Mooing Duck Aug 15 '11 at 17:28
1

It depends on what you are comparing compiler inlining to. Compared to link time or profile guided or just in time optimizations, compilers have less information to use. With less information, the compile time optimizations will be more conservative (and do less inlining overall).

A compiler will still generally be pretty decent at inlining virtual functions as it is equivalent to inlining function pointer calls (say, when you pass a free function to an STL algorithm function like sort or for_each).

MSN
  • 53,214
  • 7
  • 75
  • 105
  • I assume you mean "devirtualizing" when you wrote "inlining". I already guessed that optimizations will be more conservative; I would be interested in whether this is really true for devirtualization. – ccom Aug 13 '11 at 09:30