为什么这个简单的功能没有被虚拟化?

时间:2019-04-01 22:42:55

标签: c++ gcc compiler-optimization virtual-functions devirtualization

考虑以下代码:

struct A {
    virtual A& operator+=(const A& other) noexcept = 0;
};

void foo_inner(int *p) noexcept { *p += *p; }
void foo_virtual_inner(A *p) noexcept { *p += *p; }

void foo(int *p) noexcept
{
    return foo_inner(p);
}

struct Aint : public A {
    int i;
    A& operator+=(const A& other) noexcept override final
    { 
// No devirtualization of foo_virtual with:
        i += dynamic_cast<const Aint&>(other).i; 
// ... nor with:
//      i += reinterpret_cast<const Aint&>(other).i; 
        return *this;
    }
};

void foo_virtual(Aint *p) noexcept
{
    return foo_virtual_inner(p);
}

据我所知,foo()foo_virtual()都应编译为相同的目标代码。从operator+=调用foo_virtual_inner()时,编译器具有将虚拟化对foo_virtual中的-O3的调用所需的全部信息。但是-neither GCC 8.3, nor MSVC 19.10, nor clang 8 do this。自然,我使用了最大优化标志(/Oxfoo(int*): # @foo(int*) shl dword ptr [rdi] ret foo_virtual(Aint*): # @foo_virtual(Aint*) mov rax, qword ptr [rdi] mov rax, qword ptr [rax] mov rsi, rdi jmp rax # TAILCALL )。

为什么?这是一个错误,还是我缺少什么?


clang 8输出:

foo(int*):
        sal     DWORD PTR [rdi]
        ret
foo_virtual(Aint*):
        mov     rax, QWORD PTR [rdi]
        mov     rax, QWORD PTR [rax]
        cmp     rax, OFFSET FLAT:Aint::operator+=(A const&)
        jne     .L19
        push    rbx
        xor     ecx, ecx
        mov     edx, OFFSET FLAT:typeinfo for Aint
        mov     esi, OFFSET FLAT:typeinfo for A
        mov     rbx, rdi
        call    __dynamic_cast
        test    rax, rax
        je      .L20
        mov     eax, DWORD PTR [rax+8]
        add     DWORD PTR [rbx+8], eax
        pop     rbx
        ret
.L19:
        mov     rsi, rdi
        jmp     rax
foo_virtual(Aint*) [clone .cold.1]:
.L20:
        call    __cxa_bad_cast

GCC 8.3输出:

p$ = 8
void foo(int * __ptr64) PROC                                    ; foo
        mov     eax, DWORD PTR [rcx]
        add     eax, eax
        mov     DWORD PTR [rcx], eax
        ret     0
void foo(int * __ptr64) ENDP                                    ; foo

p$ = 8
void foo_virtual(Aint * __ptr64) PROC                  ; foo_virtual
        mov     rax, QWORD PTR [rcx]
        mov     rdx, rcx
        rex_jmp QWORD PTR [rax]
void foo_virtual(Aint * __ptr64) ENDP 

MSVC 19.10输出:

[ [-1, 0 , 1], [-1, 0 , 1], [-1, 0 , 1] ]

PS-在GCC下编译的代码中对所有typeinfo业务的解释是什么?

3 个答案:

答案 0 :(得分:1)

GCC猜测Aint * p指向Aint * p的实例(但不认为一定会发生这种情况),因此它推测性地取消了对operator + =的调用,并且typeinfo检查是它的内联副本。 -fno-devirtualize-以推测方式导致与Clang和MSVC产生的代码相同。

_Z11foo_virtualP4Aint:
.LFB4:
        .cfi_startproc
        movq    (%rdi), %rax
        movq    %rdi, %rsi
        movq    (%rax), %rax
        jmp     *%rax

答案 1 :(得分:0)

在@JanHubicka的回答之后,我针对GCC提交了一个错误:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=89924

,并且它正在(!)上工作。

答案 2 :(得分:0)

编译器无法假定Aint *实际上指向Aint对象,直到它看到否则会具有未定义语义的某些操作,例如引用其非静态成员之一。否则,可能是其他某种指针类型的reinterpret_cast结果,等待重新解释回该类型。

在我看来,将标准转换为A *应该是这样的操作,但是AFAICT标准目前并未这么说。要达到这种效果,需要考虑将其转换为正在构造的对象的非虚拟基础,这是有意允许的。