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中获取函数地址?
答案 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或者愚蠢的东西),那么,从技术上来说,虚函数调用可以在迭代之间改变。