C++:两次调用虚函数的执行时间差异

6
考虑下面的代码在 gcc 4.5.1 (Ubuntu 10.04, intel core2duo 3.0 Ghz) 下运行: 这只是两个测试,第一个测试中我直接调用虚函数,第二个测试中我通过包装类调用它:

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]

我们如何解释这样的差异呢?涉及到IT技术方面的内容。请注意,保留HTML标记。

5
为了进行性能测量并获得可靠的结果,您应该使用最高优化级别(-O3)进行编译。没有实际分析汇编代码,我的猜测是包装器被内联了,但指针通过额外的间接级别被访问。 - David Rodríguez - dribeas
4个回答

3
test2()中,程序必须首先从堆栈中加载pw,然后调用pw->execute()(这将产生调用开销),接着还要加载pw->m_print以及_p1_p4之间的参数,再加载pw的虚函数表指针,接下来是加载pw->Print在虚函数表中对应的槽,最后调用pw->Print。由于编译器无法看穿虚函数调用,所以它必须假设所有这些值都已经在下一次迭代中发生了变化,并重新加载它们。
test()中,参数在代码段中被内联,因此我们只需要加载p、虚函数表指针和虚函数表中的槽。这样就节省了五个加载操作。这很容易解释时间差异。
简而言之-加载pw->m_printpw->_p1pw->_p4 是问题的根源。

@bdonian,所以m_print、p1和p4在每次迭代中都会重新加载?这可以解释很多问题...我能做些什么吗? - codablank1
m_print,_p1,_p2,_p3和_p4都已重新加载。将它们保存到for循环级别的本地变量中可以避免这种开销,尽管这显然需要打破封装。或者,如果可以进行内联,则使pw成为本地变量(或将其复制到本地变量)可能已经足够。 - bdonlan

2

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


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

1
尽管指针的赋值是可见的,除非编译器执行整个程序优化(gcc -O2不执行),否则它不能假定从赋值到调用全局变量之间不会被重置。 - David Rodríguez - dribeas

1
你是在实际打印输出,还是只是调用一个什么都不做的名为Print的函数? 如果你正在实际打印输出,那么你就是在浪费时间。
无论如何,gprof对I/O是盲目的,所以它只会关注你的CPU使用情况。
请注意,Test2在调用之前执行了11次移动,而Test1只执行了6次。 因此,如果更多的PC样本落在Test2中,这并不奇怪。

该函数实际上什么也不做。 - codablank1
@codablank1:是的,只需看一下循环中需要执行多少条指令。 - Mike Dunlavey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接