Search code examples
c++compiler-optimizationdevirtualization

Compiler devirtualization, not too smart?


I wrote this short program to see how devirtualization would work. The compiler should be able to deduce the correct type:

#include <iostream>
using std::cout;
using std::endl;

class Base 
{
public:
    void foo() { cout << "Base::foo" << endl; }
    virtual void bar() { cout << "Base::bar" << endl; }
    virtual ~Base() = default;
};

class Child : public Base 
{
public:
    void foo() { cout << "Child::foo" << endl; }
    void bar() { cout << "Child::bar" << endl; }
};

int main()
{
    Base* obj = new Child;
    obj->foo();
    obj->bar();
    delete obj;
}

Compiled with -O2 -std=c++11 using gcc 5.3 and clang 3.7 via https://gcc.godbolt.org/.

What turned out is that neither compiler was able to optimize everything - gcc inlines foo() and makes virtual call to bar() while clang makes call to foo() and devirtualizes and inlines call to bar().

Meanwhile, if instead I call obj->bar(); and then obj->foo();, the compilers have no problem in optimizing - clang inlines both calls and gcc makes normal call to bar() instead of virtual one and inlines foo().

Can anyone explain this behavior?


Solution

  • It's probably because the compiler thinks that inlining does not help because the cout is too expensive compared to the overhead of the function call. If you replace it with something simpler, e.g. an assigment to a member, it will get inlined. See below for the output of

    #include <iostream>
    using std::cout;
    using std::endl;
    
    class Base 
    {
    public:
        void foo() { i = 1; }
        virtual void bar() { i = 2; }
        virtual ~Base() = default;
    
        int i = 0;
    };
    
    class Child : public Base 
    {
    public:
        void foo() { i = 3; }
        void bar() { i = 4; }
    };
    
    int main()
    {
        Base* obj = new Child;
        obj->foo();
        obj->bar();
        std::cout << obj->i << std::endl;
        //delete obj;
    }
    

    Assembly:

    Base::bar():
            movl    $2, 8(%rdi)
            ret
    Child::bar():
            movl    $4, 8(%rdi)
            ret
    Base::~Base():
            ret
    Child::~Child():
            ret
    Child::~Child():
            jmp     operator delete(void*)
    Base::~Base():
            jmp     operator delete(void*)
    main:
            subq    $8, %rsp
            movl    $16, %edi
            call    operator new(unsigned long)
            movl    $4, %esi
            movl    std::cout, %edi
            call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
            movq    %rax, %rdi
            call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
            xorl    %eax, %eax
            addq    $8, %rsp
            ret
            subq    $8, %rsp
            movl    std::__ioinit, %edi
            call    std::ios_base::Init::Init()
            movl    $__dso_handle, %edx
            movl    std::__ioinit, %esi
            movl    std::ios_base::Init::~Init(), %edi
            addq    $8, %rsp
            jmp     __cxa_atexit