虚函数编译器优化c ++

时间:2017-05-04 18:10:36

标签: c++ function virtual

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中获取函数地址?

3 个答案:

答案 0 :(得分:0)

我编写了一个非常小的实现,并使用g++ --save-temps opt.cpp编译它们。该标志保存了临时预处理文件,汇编文件和&amp;对象文件。我使用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;
   }

当我使用 {/ 1}}关键字运行时,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    $0, -4(%rbp)
    addl    $3, -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    $32, %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    $0, %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    $0, %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

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

答案 1 :(得分:0)

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

那么,虚函数化函数会改变可观察的行为吗?根据{{​​3}},是的。

相关段落:

  

[...]优化器必须假设[虚函数]可能   更改传递对象中的vptr。 [...]

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

这是放置新操作符的调用 - 它不分配新内存,它只是在提供的位置创建一个新对象。因此,通过在类型A的对象所在的位置构建Derived对象,我们将vptr更改为指向Derived的vtable。这个代码甚至合法吗? C ++标准说是的。“

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

根据同一篇文章,您在Clang上使用-fstrict-vtable-pointers来进行此优化,但可能会使您的代码更少符合C ++标准。

答案 2 :(得分:0)

我在godbolt.org上查了example。结果是NO,编译器都没有优化它。

以下是测试来源:

{{1}}

和生成的asm:

{{1}}

你可以看到它在每次通话时读取vtable。我想这是因为编译器无法证明你没有改变函数调用中的vtable(例如,如果你调用placement new或者愚蠢的东西),那么,从技术上来说,虚函数调用可以在迭代之间改变。