为什么我观察多重inheritance比单一更快?
我有以下两个文件:
single.cpp: –
#include <iostream> #include <stdlib.h> using namespace std; unsigned long a=0; class A { public: virtual int f() __attribute__ ((noinline)) { return a; } }; class B : public A { public: virtual int f() __attribute__ ((noinline)) { return a; } void g() __attribute__ ((noinline)) { return; } }; int main() { cin>>a; A* obj; if (a>3) obj = new B(); else obj = new A(); unsigned long result=0; for (int i=0; i<65535; i++) { for (int j=0; j<65535; j++) { result+=obj->f(); } } cout<<result<<"\n"; }
和
multiple.cpp: –
#include <iostream> #include <stdlib.h> using namespace std; unsigned long a=0; class A { public: virtual int f() __attribute__ ((noinline)) { return a; } }; class dummy { public: virtual void g() __attribute__ ((noinline)) { return; } }; class B : public A, public dummy { public: virtual int f() __attribute__ ((noinline)) { return a; } virtual void g() __attribute__ ((noinline)) { return; } }; int main() { cin>>a; A* obj; if (a>3) obj = new B(); else obj = new A(); unsigned long result=0; for (int i=0; i<65535; i++) { for (int j=0; j<65535; j++) { result+=obj->f(); } } cout<<result<<"\n"; }
我正在使用带有-O2标志的gcc 3.4.6版本
这是我得到的时间结果:
多 :-
real 0m8.635s user 0m8.608s sys 0m0.003s
单身: –
real 0m10.072s user 0m10.045s sys 0m0.001s
另一方面,如果在multiple.cpp中反转类派生的顺序:
class B : public dummy, public A {
然后我得到下面的时间(比单次inheritance稍微慢一点,正如人们所期望的那样,这要归功于代码需要对指针进行“thunk”调整): –
real 0m11.516s user 0m11.479s sys 0m0.002s
任何想法为什么这可能会发生? 就循环而言,在所有三种情况下产生的程序集似乎没有任何区别。 我还需要看一些其他的地方吗?
另外,我已经将进程绑定到一个特定的CPU核心,并且使用SCHED_RR以实时优先级运行它。
编辑: – 这是由Mysticial注意到,并由我转载。 做一个
cout << "vtable: " << *(void**)obj << endl;
就在single.cpp中的循环之前,就像公共A,公共虚拟物一样,在8.4s的单循环中也能像多个时钟一样快。
请注意,这个答案具有很强的推测性。
不同于我为“为什么X比Y慢”types的问题的其他答案,我一直无法提供可靠的证据来备份这个答案。
经过一个小时的修改之后,我认为这是由于地址alignment的三件事情:
-
obj
的地址 -
A
的虚拟方法表的地址 - 函数
f()
的地址
( owagh的回答也暗示了指令alignment的可能性。)
多inheritance比单inheritance慢的原因不是因为它“神奇”的快速,而是因为单inheritance的情况下运行到编译器或硬件“打嗝”。
如果您为单个和多个inheritance事例转储程序集,则它们在嵌套循环内是相同的(寄存器名称和所有内容)。
这是我编译的代码:
#include <iostream> #include <stdlib.h> #include <time.h> using namespace std; unsigned long a=0; #ifdef SINGLE class A { public: virtual int f() { return a; } }; class B : public A { public: virtual int f() { return a; } void g() { return; } }; #endif #ifdef MULTIPLE class A { public: virtual int f() { return a; } }; class dummy { public: virtual void g() { return; } }; class B : public A, public dummy { public: virtual int f() { return a; } virtual void g() { return; } }; #endif int main() { cin >> a; A* obj; if (a > 3) obj = new B(); else obj = new A(); unsigned long result = 0; clock_t time0 = clock(); for (int i=0; i<65535; i++) { for (int j=0; j<65535; j++) { result += obj->f(); } } clock_t time1 = clock(); cout << (double)(time1 - time0) / CLOCKS_PER_SEC << endl; cout << result << "\n"; system("pause"); // This is useless in Linux, but I left it here for a reason. }
嵌套循环的程序集在单inheritance和多inheritance情况下都是相同的:
.L5: call clock movl $65535, %r13d movq %rax, %r14 xorl %r12d, %r12d .p2align 4,,10 .p2align 3 .L6: movl $65535, %ebx .p2align 4,,10 .p2align 3 .L7: movq 0(%rbp), %rax movq %rbp, %rdi call *(%rax) cltq addq %rax, %r12 subl $1, %ebx jne .L7 subl $1, %r13d jne .L6 call clock
然而,我看到的性能差异是:
- 单人: 9.4秒
- 倍数: 8.06秒
Xeon X5482,Ubuntu,GCC 4.6.1 x64。
这使我得出结论,差异必须是数据依赖的。
如果你看看这个程序集,你会注意到可能有可变延迟的唯一指令是加载:
; %rbp = vtable movq 0(%rbp), %rax ; Dereference function pointer from vtable movq %rbp, %rdi call *(%rax) ; Call function pointer - f()
然后在调用f()
内部多访问内存。
恰好在单inheritance的例子中,上述值的偏移对处理器不利。 我不知道为什么。 但是我不得不怀疑, 这个问题的图表中的区域2与caching库冲突是类似的。
通过重新安排代码和添加虚拟函数,我可以更改这些偏移量 – 在很多情况下,这些偏移量将会消除这种减速并使单inheritance与多inheritance情况一样快。
例如,删除system("pause")
颠倒时间:
#ifdef SINGLE class A { public: virtual int f() { return a; } }; class B : public A { public: virtual int f() { return a; } void g() { return; } }; #endif #ifdef MULTIPLE class A { public: virtual int f() { return a; } }; class dummy { public: virtual void g() { return; } }; class B : public A, public dummy { public: virtual int f() { return a; } virtual void g() { return; } }; #endif int main() { cin >> a; A* obj; if (a > 3) obj = new B(); else obj = new A(); unsigned long result = 0; clock_t time0 = clock(); for (int i=0; i<65535; i++) { for (int j=0; j<65535; j++) { result += obj->f(); } } clock_t time1 = clock(); cout << (double)(time1 - time0) / CLOCKS_PER_SEC << endl; cout << result << "\n"; // system("pause"); }
- 单身: 8.06秒
- 多个: 9.4秒
我想我至less得到了一些进一步的领导,为什么这可能会发生。 循环的程序集完全相同,但目标文件不是!
对于最初的循环(即)
cout << "vtable: " << *(void**)obj << endl; for (int i=0; i<65535; i++) { for (int j=0; j<65535; j++) { result+=obj->f(); } }
我在对象文件中得到以下内容:
40092d: bb fe ff 00 00 mov $0xfffe,%ebx 400932: 48 8b 45 00 mov 0x0(%rbp),%rax 400936: 48 89 ef mov %rbp,%rdi 400939: ff 10 callq *(%rax) 40093b: 48 98 cltq 40093d: 49 01 c4 add %rax,%r12 400940: ff cb dec %ebx 400942: 79 ee jns 400932 <main+0x42> 400944: 41 ff c5 inc %r13d 400947: 41 81 fd fe ff 00 00 cmp $0xfffe,%r13d 40094e: 7e dd jle 40092d <main+0x3d>
但是,如果没有cout,循环会变成: – (.cpp第一个)
for (int i=0; i<65535; i++) { for (int j=0; j<65535; j++) { result+=obj->f(); } }
现在,.obj: –
400a54: bb fe ff 00 00 mov $0xfffe,%ebx 400a59: 66 data16 400a5a: 66 data16 400a5b: 66 data16 400a5c: 90 nop 400a5d: 66 data16 400a5e: 66 data16 400a5f: 90 nop 400a60: 48 8b 45 00 mov 0x0(%rbp),%rax 400a64: 48 89 ef mov %rbp,%rdi 400a67: ff 10 callq *(%rax) 400a69: 48 98 cltq 400a6b: 49 01 c4 add %rax,%r12 400a6e: ff cb dec %ebx 400a70: 79 ee jns 400a60 <main+0x70> 400a72: 41 ff c5 inc %r13d 400a75: 41 81 fd fe ff 00 00 cmp $0xfffe,%r13d 400a7c: 7e d6 jle 400a54 <main+0x64>
所以我不得不说这不是真的由于Mysticial指出错误的别名,而只是由于编译器/链接器发出的这些NOP。
在这两种情况下的大会是:
.L30: movl $65534, %ebx .p2align 4,,7 .L29: movq (%rbp), %rax movq %rbp, %rdi call *(%rax) cltq addq %rax, %r12 decl %ebx jns .L29 incl %r13d cmpl $65534, %r13d jle .L30
现在,.p2align 4,7将插入数据/ NOP,直到下一条指令的指令计数器最后四位0,最多7个NOP。 现在在p2align之后的指令的地址在没有cout的情况下和在填充之前是
0x400a59 = 0b101001011001
而且由于它需要<= 7个NOP来alignment下一个指令,所以它实际上将在目标文件中这样做。
另一方面,对于cout的情况下,在.p2align之后的指令登陆
0x400932 = 0b100100110010
而且要用大于7的NOP来填充它才能被16的边界整除。 因此,它不这样做。
所以额外的时间只是由于编译器用-O2标志编译时加上代码(为了更好的高速cachingalignment),而不是真正由于假别名造成的。
我认为这解决了这个问题。 我使用http://sourceware.org/binutils/docs/as/P2align.html作为我的参考;.p2align实际上做了什么。
这个答案更具推测性。
经过5分钟的修改和阅读Mysticials回答后,得出的结论是,这是一个硬件问题:在热循环中生成的代码基本上是相同的,所以这不是编译器的问题,硬件作为唯一的嫌疑人。
一些随意的想法:
- 分支预测
- 分支(=函数)目标地址的alignment或部分混叠
- 一次读取相同地址后,L1caching运行热点
- 宇宙射线
用你当前的代码,编译器可以自由地将对obj->f()
的调用虚拟化,因为obj
不能有除class B
之外的任何dynamictypes。
我build议
if (a>3) { B* objb = new B(); objb->a = 5; obj = objb; } else obj = new A();
我的猜测是class B : public dummy, public A
对A
而言是不利的。 填充dummy
到16个字节,看看是否有区别。