我有这个C ++,它创建了两个派生对象,然后多次调用虚函数调用:
Parent* d;
Child1 d1[1];
Child2 d2[1];
if(__rdtsc() & 1 != 0){
d = d1;
}
else{
d = d2;
}
for(unsigned long long i =0; i<9000000000; ++i){
sum += d->process2();
}
它生成这个汇编程序:
for(unsigned long long i =0; i<9000000000; ++i){
000000013F4241A5 mov qword ptr [rsp+100h],0
000000013F4241B1 jmp main+2B6h (013F4241C6h)
000000013F4241B3 mov rax,qword ptr [rsp+100h]
000000013F4241BB inc rax
000000013F4241BE mov qword ptr [rsp+100h],rax
000000013F4241C6 mov rax,218711A00h
000000013F4241D0 cmp qword ptr [rsp+100h],rax
000000013F4241D8 jae main+306h (013F424216h)
sum += d->process2();
000000013F4241DA mov rax,qword ptr [rsp+0F8h]
000000013F4241E2 mov rax,qword ptr [rax]
000000013F4241E5 mov rcx,qword ptr [rsp+0F8h]
000000013F4241ED call qword ptr [rax+8]
000000013F4241F0 mov qword ptr [rsp+1D8h],rax
000000013F4241F8 mov rax,qword ptr [rsp+1D8h]
000000013F424200 mov rcx,qword ptr [sum (013F4385D8h)]
000000013F424207 add rcx,rax
000000013F42420A mov rax,rcx
000000013F42420D mov qword ptr [sum (013F4385D8h)],rax
}
基于汇编程序,有人可以确认编译器无法优化循环中的虚拟调用(即使每次迭代都调用相同的派生对象),因为编译器无法知道d1
还是{{1}因为调用d2
只能在运行时解析而被选中了吗?
(如果有人可以给我一些建议如何阅读__rdtsc()
电话的汇编程序,那将是非常感激的)
答案 0 :(得分:5)
000000013F4241DA mov rax,qword ptr [rsp+0F8h] //load "this" into rax
000000013F4241E2 mov rax,qword ptr [rax] //load vtable pointer
000000013F4241E5 mov rcx,qword ptr [rsp+0F8h] //load "this" into rcx
000000013F4241ED call qword ptr [rax+8] //call second entry in vtable?
显然,对虚拟功能的调用并未优化掉。你是对的,这是因为随机因素。
答案 1 :(得分:1)
编译器无法内联虚拟调用,因为是的,它无法知道将使用哪个对象d1或d2,因此允许两个可能的内联结果。此外,作为虚拟调用,vtable查找可能会有额外的开销。
我的建议,如果你想尝试优化自己,那就是写一些类似于
的东西if(__rdtsc() & 1 != 0){
for(unsigned long long i =0; i<9000000000; ++i){
sum += d1[0].process2();
}
}
else{
for(unsigned long long i =0; i<9000000000; ++i){
sum += d2[0].process2();
}
}
虽然如果process2是虚拟调用,仍然可能无法优化,并且总是有可能不会发生内联。
总而言之,虚拟调用总是增加开销,如果时钟周期很重要,最好避免。您可能会考虑Static Polymorphism,它会失去一些灵活性,但可以将成本从运行时转移到编译时。
编辑以响应下面的user997112: 静态多态性不能完全适用于上述情况,但可以用来简化我的示例,但将for循环放在函数中:
void iterate_a_bunch( Parent<Child> &f )
{
for(unsigned long long i =0; i<9000000000; ++i){
f.process2();
}
}
此函数将编译两次,一次针对Child1,一次针对Child2,导致代码量更大,但可能会增加运行时。
答案 2 :(得分:0)
我使用g ++进行了实验,并使用-O3进行了优化。但是,我必须说优化与dvntehn00bz在答案中提供的内容完全相同。
400860: 48 8b 45 00 mov 0x0(%rbp),%rax
400864: 48 89 ef mov %rbp,%rdi
400867: ff 50 10 callq *0x10(%rax)
40086a: 48 83 eb 01 sub $0x1,%rbx
40086e: 75 f0 jne 400860 <main+0x40>
整个代码有两个循环,完全自动。
P.S。当然,你的类可能比我的瘦版本复杂得多,因此编译器的优化肯定要容易得多。