【问题标题】:Virtual function compiler optimization c++虚函数编译优化c++
【发布时间】:2017-10-03 01:16:27
【问题描述】:
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中获取函数地址?

【问题讨论】:

  • 这个问题的答案很可能取决于您的编译器、编译器版本和编译标志。只要不改变定义的行为,优化主要由实现自行决定。
  • 简而言之,允许优化它,如果这是您的担忧。如果没有,请随时向您的编译器提供商发送错误报告
  • 强制优化的规则通常会限制可调试性。 C++ 通常更喜欢在有意义的情况下让代码生成“实现定义”或“未定义行为”
  • 我刚刚在 gcc 6.3.0 (Debian) 上尝试过,它确实为每次迭代获取函数指针。有趣的是,它似乎确实有一些优化,它将获取的指针与Base::fnc 的地址进行比较,如果比较相等,则跳过调用。
  • 有一点需要注意的是,如果我没记错的话,fnc技术上是允许通过放置new来改变*this的动态类型,因此编译器必须对去虚拟化保持保守。但是,我相信 Clang 提供了一个扩展来让编译器假设这永远不会发生。来源:blog.llvm.org/2017/03/devirtualization-in-llvm-and-clang.html

标签: c++ function virtual


【解决方案1】:

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

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

相关段落:

[...] 优化器将不得不假设 [virtual function] 可能 更改传递对象中的 vptr。 [...]

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

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

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

根据同一篇文章,您在 Clang 上使用 -fstrict-vtable-pointers 来允许这种优化,但有可能使您的代码不太符合 C++ 标准。

【讨论】:

  • 如果有人可以帮助我进行格式化,我们将不胜感激
  • 我想知道是否允许在另一个继续使用 this 指针的方法中调用替换 *this 的方法。 AFAIK,如果你访问一个生命周期已经结束的对象,它就是 UB。您必须使用从放置 new 获得的指针来访问新对象。我的推理是 process 方法的隐式 this 指针仍然指向旧对象,因此编译器可能会认为它仍然有效,但我可能错了。
  • 实际上,源代码中的以下段落是同一行,因此编译器应该能够假定这在使用时仍然有效。
  • 看,这个答案是一年前的,所以我可能遗漏了一些东西。但是在相同类型的旧对象的存储中创建新对象通常“神奇地”使指向旧对象的指针指向新对象。值得注意的例外是具有const 或引用数据成员的类型......我不确定 vptrs 是否算作前者。总的来说,/耸耸肩
【解决方案2】:

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

这里是测试源:

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();
}

以及生成的asm:

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 或其他愚蠢的东西),所以从技术上讲,虚函数调用可能会在迭代之间发生变化。

【讨论】:

  • 我真的很惊讶。有点。
  • 那真是太糟糕了
【解决方案3】:

我编写了一个非常小的实现并使用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
    .对齐 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)
    添加 $3, -4(%rbp)
    movl -4(%rbp), %eax
    流行音乐%rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size _ZN4Base3fncEi, .-_ZN4Base3fncEi
    。文本
    .globl 主要
    .type 主要,@function
主要的:
.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)
    移动 $0, %eax
    movq -8(%rbp), %rdx
    xorq %fs:40, %rdx
    耶.L5
    调用 __stack_chk_fail@PLT
.L5:
    离开
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size 主要,.-main
    .weak _ZTV4Base
    .section .data.rel.ro.local._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
    .align 8
    .type _ZTV4Base, @object
    .size _ZTV4Base, 24
_ZTV4基地:
    .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
_ZTI4基地:
    .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” 。文本 .globl 主要 .type 主要,@function 主要的: .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) 移动 $0, %eax 流行音乐%rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE2: .size 主要,.-main .ident“GCC:(Ubuntu 6.2.0-5ubuntu12)6.2.0 20161005” .section .note.GNU-stack,"",@progbits

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

【讨论】:

    猜你喜欢
    • 2018-11-21
    • 1970-01-01
    • 2014-02-21
    • 1970-01-01
    • 2019-04-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多