Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't gcc devirtualize this function call?

#include <cstdio>
#include <cstdlib>
struct Interface {
    virtual void f() = 0;
};

struct Impl1: Interface {
    void f() override {
        std::puts("foo");
    }
};

// or __attribute__ ((visibility ("hidden")))/anonymous namespace
static Interface* const ptr = new Impl1 ;

int main() {
    ptr->f();
}

When compiled with g++-7 -O3 -flto -fdevirtualize-at-ltrans -fipa-pta -fuse-linker-plugin, the above ptr->f() call cannot be devirtualized.

It seems that no external library can modify ptr. Is this a deficiency of GCC optimizer, or because some other sources make devirtualization unavailable in this case?

Godbolt link

UPDATE: It seems that clang-7 with -flto -O3 -fwhole-program-vtables -fvisibility=hidden is the only compiler+flags (as in 2018/03) that can devirtualize this program.

like image 632
lz96 Avatar asked Feb 21 '18 12:02

lz96


2 Answers

If you move the ptr into the main function, the result is very telling and offers a strong hint as to why gcc doesn't want to de-virtualize the pointer.

The disassembly for this shows that if the 'has the static been initialized flag' is false, it initializes the static and then jumps right back to the virtual function call, even though nothing could possibly have happened to it in between.

This tells me that gcc is hard-wired to believe that any kind of globally persistent pointer must always be treated as a pointer to an unknown type.

In fact, it's even worse than this. If you add in a local variable, it matters whether the call to the f on the static pointer occurs between the creation of the local variable and the call to f or not. The assembly showing the f interposed case is here: Another godbolt link; and it is simple to re-arrange it yourself on the site to see how the assembly turns into an inline of f once the other call isn't interposed.

So, gcc must assume that the actual type a pointer refers to may change whenever control flow leaves the function for any reason. And whether or not it's declared const is irrelevant. Nor is it relevant if it's address is ever taken, or any number of other things.

clang does the same thing. This seems overly cautious to me, but I'm not a compiler writer.

like image 128
Omnifarious Avatar answered Nov 03 '22 15:11

Omnifarious


I just made an other experiment, as in Omnifarious answer. But this time I make the pointer point to a static object:

Impl1 x;
static Interface* const ptr = &x ;

GCC do devirtualize the function call, -O2 is sufficient. I don't see any rule in the C++ standard that would make pointer to static storage treated differently than pointer to dynamic storage.

It is allowed to change the object at the address pointed to by ptr. So the compiler must track what is happening at that address to know what is the actual dynamic type of the object. So my opinion is that optimizer implementer may have considered that tracking what is happening on the heap would be too difficult in real program, so the compiler just don't do it.

like image 33
Oliv Avatar answered Nov 03 '22 16:11

Oliv