为什么我观察多重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 AA而言是不利的。 填充dummy到16个字节,看看是否有区别。