【问题标题】:C++ : difference of execution time between two call of a virtual functionC ++:两次调用虚函数之间的执行时间差异
【发布时间】:2011-12-06 19:54:17
【问题描述】:

gcc 4.5.1(Ubuntu 10.04,intel core2duo 3.0 Ghz)下考虑此代码 这只是 2 次测试,第一次我直接调用 virtual fucnion,第二次我通过 Wrapper 类调用它:

test.cpp

#define ITER 100000000

class Print{

public:

typedef Print* Ptr;

virtual void print(int p1, float p2, float p3, float p4){/*DOES NOTHING */}

};

class PrintWrapper
{

    public:

      typedef PrintWrapper* Ptr;

      PrintWrapper(Print::Ptr print, int p1, float p2, float p3, float p4) :
      m_print(print), _p1(p1),_p2(p2),_p3(p3),_p4(p4){}

      ~PrintWrapper(){}

      void execute()
      { 
        m_print->print(_p1,_p2,_p3,_p4); 
      }

    private:

      Print::Ptr m_print;
      int _p1;
      float _p2,_p3,_p4;

};

 Print::Ptr p = new Print();
 PrintWrapper::Ptr pw = new PrintWrapper(p, 1, 2.f,3.0f,4.0f);

void test1()
{

 //-------------test 1-------------------------

 for (auto var = 0; var < ITER; ++var) 
 {
   p->print(1, 2.f,3.0f,4.0f);
 }

 }

 void test2()
 {

  //-------------test 2-------------------------

 for (auto var = 0; var < ITER; ++var) 
 {
   pw->execute();
 }

}

int main() 
{ 
  test1(); 
  test2();
}

我使用 gprof 和 objdump 对其进行了分析:

g++ -c -std=c++0x -pg -g -O2 test.cpp
objdump -d -M intel -S test.o > objdump.txt
g++ -pg test.o -o test
./test
gprof test > gprof.output

在 gprof.output 中我观察到 test2() 比 test1() 花费更多时间,但我无法解释

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 49.40      0.41     0.41        1   410.00   540.00  test2()
 31.33      0.67     0.26 200000000     0.00     0.00  Print::print(int, float, float, float)
 19.28      0.83     0.16        1   160.00   290.00  test1()
  0.00      0.83     0.00        1     0.00     0.00  global constructors keyed to p

objdump.txt 中的汇编代码对我也没有帮助:

 //-------------test 1-------------------------
 for (auto var = 0; var < ITER; ++var) 
  15:   83 c3 01                add    ebx,0x1
 {
   p->print(1, 2.f,3.0f,4.0f);
  18:   8b 10                   mov    edx,DWORD PTR [eax]
  1a:   c7 44 24 10 00 00 80    mov    DWORD PTR [esp+0x10],0x40800000
  21:   40 
  22:   c7 44 24 0c 00 00 40    mov    DWORD PTR [esp+0xc],0x40400000
  29:   40 
  2a:   c7 44 24 08 00 00 00    mov    DWORD PTR [esp+0x8],0x40000000
  31:   40 
  32:   c7 44 24 04 01 00 00    mov    DWORD PTR [esp+0x4],0x1
  39:   00 
  3a:   89 04 24                mov    DWORD PTR [esp],eax
  3d:   ff 12                   call   DWORD PTR [edx]

  //-------------test 2-------------------------
 for (auto var = 0; var < ITER; ++var) 
  65:   83 c3 01                add    ebx,0x1

      ~PrintWrapper(){}

      void execute()
      { 
        m_print->print(_p1,_p2,_p3,_p4); 
  68:   8b 10                   mov    edx,DWORD PTR [eax]
  6a:   8b 70 10                mov    esi,DWORD PTR [eax+0x10]
  6d:   8b 0a                   mov    ecx,DWORD PTR [edx]
  6f:   89 74 24 10             mov    DWORD PTR [esp+0x10],esi
  73:   8b 70 0c                mov    esi,DWORD PTR [eax+0xc]
  76:   89 74 24 0c             mov    DWORD PTR [esp+0xc],esi
  7a:   8b 70 08                mov    esi,DWORD PTR [eax+0x8]
  7d:   89 74 24 08             mov    DWORD PTR [esp+0x8],esi
  81:   8b 40 04                mov    eax,DWORD PTR [eax+0x4]
  84:   89 14 24                mov    DWORD PTR [esp],edx
  87:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
  8b:   ff 11                   call   DWORD PTR [ecx]

我们如何解释这种差异?

【问题讨论】:

  • 要进行性能测量并获得合理的结果,您应该使用最高优化级别 (-O3) 进行编译。在没有实际分析程序集的情况下,我的猜测是包装器是内联的,但指针是通过额外的间接级别访问的。

标签: c++ performance function virtual c++11


【解决方案1】:

test2()中,程序必须首先从堆中加载pw,然后调用pw-&gt;execute()(这会产生调用开销),然后加载pw-&gt;m_print以及_p1_p4参数,然后为pw加载vtable指针,然后为pw-&gt;Print加载vtable槽,然后调用pw-&gt;Print。由于编译器无法看穿虚拟调用,因此它必须假定所有这些值在下一次迭代中都已更改,并重新加载它们。

test() 中,参数是内联在代码段中的,我们只需要加载p、vtable 指针和vtable 槽。我们以这种方式节省了五次负载。这很容易解释时差。

简而言之 - pw-&gt;m_printpw-&gt;_p1pw-&gt;_p4 的负载是这里的罪魁祸首。

【讨论】:

  • @bdonian 所以 m_print、p1 和 p4 在每次迭代时都会重新加载??会解释很多...有什么我可以做的吗?
  • m_print、_p1、_p2、_p3 和 _p4 都重新加载。在 for 循环级别将它们保存到本地可以让您避免这种开销,尽管显然这需要打破封装。或者,如果可以进行内联,则将 pw 设为本地(或将其复制到本地)可能就足够了。
【解决方案2】:

一个区别是,您在 test1 中传递给 print 的值将存储在指令本身中,而 PrintWrapper 中的内容必须从堆中加载。您可以在汇编程序中看到这一点。由于这个原因,可能会遇到不同的内存访问时间。

【讨论】:

    【解决方案3】:

    在直接调用中,编译器可以优化掉函数的虚拟性,因为p 的类型在编译时是已知的(因为对p 的唯一赋值是可见的)。在PrintWrapper中,类型被擦除,必须执行虚函数调用。

    【讨论】:

    • 虽然 e 对指针的赋值是可见的,除非编译器执行整个程序优化(gcc -O2 没有)它不能假设从赋值到调用 global 变量不会被重置。
    【解决方案4】:

    您实际上是在打印,还是只是调用了一个什么都不做的名为 Print 的函数? 如果您实际上是在打印,那么您就是在称量猪的头发。

    无论如何,gprof 对 I/O 视而不见,因此它只查看您的 CPU 使用率。

    注意,Test2 在跟注前做了 11 步,而 Test1 只做了 6 步。 因此,如果更多的 PC 样本进入 Test2,那就不足为奇了。

    【讨论】:

    • 函数实际上什么都不做;
    • @codablank1:对,所以看看它在循环中需要执行多少条指令。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-14
    • 2015-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-16
    相关资源
    最近更新 更多