虚函数编译优化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 或其他愚蠢的东西),因此,从技术上讲,虚拟函数调用可以在迭代之间更改。
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 或其他愚蠢的东西),因此,从技术上讲,虚拟函数调用可以在迭代之间更改。