虚函数编译优化c++

Virtual function compiler optimization c++

class Base 
{
public:
    virtual void fnc(size_t nm) 
    {
        // do some work here
    }

    void process()
    {
        for(size_t i = 0; i < 1000; i++)
        {
            fnc(i);
        }
    }
}  

c++ 编译器能否并且将优化从进程函数调用 fnc 函数,考虑到它每次在循环内调用时都是相同的函数? 还是每次调用函数时都会从vtable中获取函数地址?

我写了一个非常小的实现并使用 g++ --save-temps opt.cpp 编译了它们。该标志保留了临时预处理文件、程序集文件和目标文件。我 运行 一次使用 virtual 关键字,一次不使用。这是程序。

class Base
{
    public:
        virtual int fnc(int nm)
        {
           int i = 0;
           i += 3;
           return i;
        }

        void process()
        {
           int x = 9;
           for(int i = 0; i < 1000; i++)
           {
              x += i;
           }
       }
   };

   int main(int argc, char* argv[]) {
       Base b;

       return 0;
   }

当我 运行 使用 关键字 virtual 时,在 x86_64 Linux 框上生成的程序集是:

.file  "opt.cpp"
    .section    .text._ZN4Base3fncEi,"axG",@progbits,_ZN4Base3fncEi,comdat
    .align 2
    .weak   _ZN4Base3fncEi
    .type   _ZN4Base3fncEi, @function
_ZN4Base3fncEi:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -24(%rbp)
    movl    %esi, -28(%rbp)
    movl    [=15=], -4(%rbp)
    addl    , -4(%rbp)
    movl    -4(%rbp), %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   _ZN4Base3fncEi, .-_ZN4Base3fncEi
    .text
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    , %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    leaq    16+_ZTV4Base(%rip), %rax
    movq    %rax, -16(%rbp)
    movl    [=15=], %eax
    movq    -8(%rbp), %rdx
    xorq    %fs:40, %rdx
    je  .L5
    call    __stack_chk_fail@PLT
.L5:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .weak   _ZTV4Base
    .section    .data.rel.ro.local._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
    .align 8
    .type   _ZTV4Base, @object
    .size   _ZTV4Base, 24
_ZTV4Base:
    .quad   0
    .quad   _ZTI4Base
    .quad   _ZN4Base3fncEi
    .weak   _ZTI4Base
    .section    .data.rel.ro._ZTI4Base,"awG",@progbits,_ZTI4Base,comdat
    .align 8
    .type   _ZTI4Base, @object
    .size   _ZTI4Base, 16
_ZTI4Base:
    .quad   _ZTVN10__cxxabiv117__class_type_infoE+16
    .quad   _ZTS4Base
    .weak   _ZTS4Base
    .section    .rodata._ZTS4Base,"aG",@progbits,_ZTS4Base,comdat
    .type   _ZTS4Base, @object
    .size   _ZTS4Base, 6
_ZTS4Base:
    .string "4Base"
    .ident  "GCC: (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005"
    .section    .note.GNU-stack,"",@progbits

没有 virtual 关键字,最后的汇编是:

    .file   "opt.cpp"
    .text
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    [=16=], %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005"
    .section    .note.GNU-stack,"",@progbits

现在与已发布的问题不同,此示例甚至不使用虚拟方法,并且生成的程序集要大得多。我没有尝试使用优化进行编译,但试一试。

通常,允许编译器优化任何不改变程序可观察行为的内容。有一些例外,例如从函数返回时省略非平凡的复制构造函数,但可以假设在 C++ 抽象机中不改变输出或程序的副作用的预期代码生成的任何变化都可以由编译器完成。

那么,去虚拟化一个函数可以改变可观察的行为吗?根据this article,是的。

相关段落:

[...] optimizer will have to assume that [virtual function] might change the vptr in passed object. [...]

void A::foo() { // virtual 
 static_assert(sizeof(A) == sizeof(Derived)); 
 new(this) Derived; 
}

This is call of placement new operator - it doesn’t allocate new memory, it just creates a new object in the provided location. So, by constructing a Derived object in the place where an object of type A was living, we change the vptr to point to Derived’s vtable. Is this code even legal? C++ Standard says yes."

因此,如果编译器无法访问虚函数的定义(并且在编译类型时知道*this的具体类型),那么这种优化是有风险的。

根据同一篇文章,您在 Clang 上使用 -fstrict-vtable-pointers 以允许进行此优化,但存在使您的代码不符合 C++ 标准的风险。

我在 godbolt.org 上检查了一个 example。结果是 NO,none 编译器对其进行了优化。

这是测试源:

class Base 
{
public:
// made it pure virtual to decrease clutter
    virtual void fnc(int nm) =0;
    void process()
    {
        for(int i = 0; i < 1000; i++)
        {
            fnc(i);
        }
    }
};

void test(Base* b ) {
    return b->process();
}

和生成的汇编:

test(Base*):
        push    rbp       ; setup function call 
        push    rbx
        mov     rbp, rdi  ; Base* rbp 
        xor     ebx, ebx  ; int ebx=0;
        sub     rsp, 8    ; advance stack ptr
.L2:
        mov     rax, QWORD PTR [rbp+0]  ; read 8 bytes from our Base*
                                        ; rax now contains vtable ptr 
        mov     esi, ebx                ; int parameter for fnc
        add     ebx, 1                  ; i++
        mov     rdi, rbp                ; (Base*) this parameter for fnc
        call    [QWORD PTR [rax]]       ; read vtable and call fnc
        cmp     ebx, 1000               ; back to the top of the loop 
        jne     .L2
        add     rsp, 8                  ; reset stack ptr and return
        pop     rbx
        pop     rbp
        ret

如您所见,它会在每次调用时读取 vtable。我想这是因为编译器无法证明您没有在函数调用中更改 vtable(例如,如果您调用 placement new 或其他愚蠢的东西),因此,从技术上讲,虚拟函数调用可以在迭代之间更改。