C ++: разница времени выполнения между двумя вызовами виртуальной функции - PullRequest
6 голосов
/ 06 декабря 2011

Рассмотрим этот код в gcc 4.5.1 (Ubuntu 10.04, intel core2duo 3.0 Ghz) Это всего лишь 2 теста, в первом я делаю прямой вызов виртуального 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]

Как мы можем объяснить такую ​​разницу?

Ответы [ 4 ]

3 голосов
/ 07 декабря 2011

В test2() программа должна сначала загрузить pw из кучи, затем вызвать pw->execute() (что связано с издержками вызова), затем загрузить pw->m_print, а также аргументы от _p1 до _p4, затем загрузите указатель vtable для pw, затем загрузите слот vtable для pw->Print, затем вызовите pw->Print. Поскольку компилятор не может видеть виртуальный вызов, он должен предположить, что все эти значения были изменены для следующей итерации, и перезагрузить их все.

В test() аргументы встроены в сегмент кода, и нам нужно только загрузить p, указатель vtable и слот vtable. Мы сохранили пять загрузок таким образом. Это может легко объяснить разницу во времени.

Короче говоря, здесь виноваты нагрузки от pw->m_print и pw->_p1 до pw->_p4.

2 голосов
/ 07 декабря 2011

Одно отличие состоит в том, что значения, которые вы передаете в печать в test1, будут сохранены в самих инструкциях, тогда как содержимое в PrintWrapper должно быть загружено из кучи. Вы можете видеть это в ассемблере. По этой причине может быть запущено другое время доступа к памяти.

1 голос
/ 07 декабря 2011

Вы действительно печатаете или просто вызываете функцию с именем Print, которая ничего не делает? Если вы на самом деле печатаете, вы взвешиваете волосы на борове.

Несмотря на это, gprof не видит операций ввода-вывода, поэтому он смотрит только на использование вашего процессора.

Обратите внимание, Test2 делает 11 ходов до вызова, в то время как Test1 делает только 6. Так что, если в Test2 появится больше образцов ПК, это не удивительно.

1 голос
/ 06 декабря 2011

При прямом вызове компилятор может оптимизировать виртуальность функции, потому что тип p известен во время компиляции (потому что видно только одно присвоение p). В PrintWrapper тип стирается, и должен выполняться вызов виртуальной функции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...