44

我有以下两个文件:-

单.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";                                                                             
}

多个.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 {

然后我得到以下时间(由于代码需要对 this 指针进行“thunk”调整,这比单继承的时间要慢一些):-

real    0m11.516s
user    0m11.479s
sys 0m0.002s

知道为什么会发生这种情况吗?就循环而言,为所有三种情况生成的程序集似乎没有任何区别。我还有其他地方需要看吗?

此外,我已将进程绑定到特定的 cpu 内核,并使用 SCHED_RR 以实时优先级运行它。

编辑:- 这被 Mysticial 注意到并由我复制。做一个

cout << "vtable: " << *(void**)obj << endl;

就在 single.cpp 中的循环导致 single 也像 public A、public dummy 一样以 8.4 秒的速度进入多个时钟之前。

4

5 回答 5

27

请注意,这个答案是高度推测性的。

与我对“为什么 X 比 Y 慢”类型的问题的其他一些答案不同,我无法提供确凿的证据来支持这个答案。


在修补了大约一个小时之后,我认为这是由于三件事的地址对齐:

owagh 的回答也暗示了指令对齐的可能性。)

多重继承比单继承慢的原因不是因为它“神奇地”快,而是因为单继承情况正在运行到编译器或硬件“打嗝”。


如果您为单继承和多继承情况转储程序集,则它们在嵌套循环中是相同的(寄存器名称和所有内容)。

这是我编译的代码:

#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.
}

嵌套循环的程序集在单继承和多继承情况下都是相同的:

.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 秒

至强 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().


恰好在单继承示例中,上述值的偏移量对处理器不利。我不知道为什么。但我不得不怀疑一些事情,这将是缓存库冲突,其方式与此问题图中的区域 2类似。

通过重新排列代码和添加虚拟函数,我可以更改这些偏移量——在很多情况下,这将消除这种减速并使单继承与多继承情况一样快。


例如,删除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 秒
于 2012-05-03T23:21:23.283 回答
16

我想我至少对为什么会发生这种情况有了进一步的了解。循环的程序集完全相同,但目标文件却不同!

对于首先使用 cout 的循环(即)

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 first)

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。现在,在没有 cout 的情况下 p2align 之后和填充之前的指令地址将是

0x400a59 = 0b101001011001

而且由于它需要 <=7 NOP 来对齐下一条指令,它实际上会在目标文件中这样做。

另一方面,对于 cout 的情况,.p2align 之后的指令落在

0x400932 = 0b100100110010

并且需要> 7个 NOP 才能将其填充到可被 16 整除的边界。因此,它不这样做。

因此,花费的额外时间仅仅是由于编译器在使用 -O2 标志编译时填充代码的 NOP(为了更好的缓存对齐),而不是由于错误的别名。

我认为这可以解决问题。我使用http://sourceware.org/binutils/docs/as/P2align.html 作为 .p2align 实际作用的参考。

于 2012-05-07T15:30:21.973 回答
5

这个答案更具推测性。

在对此进行了 5 分钟的修改并阅读了 Mysticials 的答案后,得出的结论是这是一个硬件问题:热循环中生成的代码基本相同,因此编译器没有问题,硬件为唯一的嫌疑人。

一些随意的想法:

  • 分支预测
  • 分支(=函数)目标地址的对齐或部分别名
  • 一直读取同一个地址后,L1 缓存过热
  • 宇宙射线
于 2012-05-04T00:22:57.060 回答
1

使用您当前的代码,编译器可以自由地去虚拟化对 的调用obj->f(),因为除了 .obj之外不能有任何动态类型class B

我建议

if (a>3) {
    B* objb = new B();
    objb->a = 5;
    obj = objb;
}
else
    obj = new A();
于 2012-05-03T20:52:54.083 回答
0

就目前而言,我的猜测是class B : public dummy, public A不利的对齐方式A。填充dummy到 16 个字节,看看是否有区别。

于 2012-05-03T20:51:58.027 回答