C ++: verschil in uitvoeringstijd tussen twee oproepen van een virtuele functie

Overweeg deze code onder gcc 4.5.1 (Ubuntu 10.04, intel core2duo 3.0 Ghz) Het zijn slechts 2 tests, in de eerste maak ik een directe aanroep op virtuele fucnion en in de tweede noem ik het via een Wrapper-klasse:

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();
}

Ik heb het geprofileerd met gprof en 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

in gprof.output merkte ik dat test2() meer tijd kost dan test1 (), maar ik kan het niet uitleggen

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

De assembly-code in objdump.txt helpt mij ook niet:

 //-------------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]

Hoe kunnen we zo'n verschil verklaren?

6
Om prestatiemeting uit te voeren en zinvolle resultaten te krijgen, moet u compileren met het hoogste optimalisatieniveau (-O3). Zonder de assembly daadwerkelijk te analyseren, vermoed ik dat de wikkel inline is maar de aanwijzer toegankelijk is via een extra niveau van indirectie.
toegevoegd de auteur David Rodríguez - dribeas, de bron

4 antwoord

In test2(), the program must first load pw from the heap, then call pw->execute() (which incurs call overhead), then load pw->m_print as well as the _p1 through _p4 arguments, then load the vtable pointer for pw, then load the vtable slot for pw->Print, then call pw->Print. Because the compiler can't see through the virtual call, it then must assume all of these values have changed for the next iteration, and reload them all.

In test() staan ​​de argumenten inline in het codesegment en hoeven we alleen p , de vtable pointer en de vtable slot te laden. We hebben vijf ladingen op deze manier opgeslagen. Dit zou gemakkelijk het tijdsverschil kunnen verklaren.

In short - the loads of pw->m_print and pw->_p1 through pw->_p4 are the culprit here.

3
toegevoegd
m_print, _p1, _p2, _p3 en _p4 zijn allemaal opnieuw geladen. Door ze te bewaren bij de lokale bevolking op het niveau van de for-lus, kun je deze overhead vermijden, hoewel dit uiteraard een inbrekende inkapseling vereist. Als alternatief, als inlining mogelijk is, kan het voldoende zijn om pw lokaal te maken (of naar een lokaal te kopiëren).
toegevoegd de auteur bdonlan, de bron
@bdonian dus m_print, p1 en p4 worden herladen bij elke iteratie ?? zou veel verklaren ... Kan ik er iets aan doen?
toegevoegd de auteur codablank1, de bron

Een verschil is dat de waarden die u in test1 doorgeeft, in de instructies zelf worden opgeslagen, terwijl de spullen in PrintWrapper van de heap moeten worden geladen. Je kunt dit zien aan de assembler. Kan om die reden verschillende toegangstijden voor geheugens tegenkomen.

2
toegevoegd

Drukt u eigenlijk af of belt u gewoon een functie met de naam Afdrukken die niets doet? Als je aan het printen bent, weeg je het haar op het varken.

Hoe dan ook, gprof is blind voor I/O, dus het kijkt alleen naar je CPU-gebruik.

Let op, Test2 doet 11 zetten voor het gesprek, terwijl Test1 slechts 6 bewegingen maakt. Dus als er meer pc-samples in Test2 landen, is dat niet verrassend.

1
toegevoegd
@ codablank1: Rechts, dus kijk maar naar hoeveel instructies het in de loop moet uitvoeren.
toegevoegd de auteur Mike Dunlavey, de bron
de functie doet eigenlijk niets;
toegevoegd de auteur codablank1, de bron

In de directe aanroep kan de compiler de virtualiteit van de functie wegwerken, omdat het type p bekend is tijdens het compileren (omdat de enige toewijzing aan p zichtbaar is). In PrintWrapper wordt het type gewist en moet de virtuele functieaanroep worden uitgevoerd.

1
toegevoegd
Hoewel de toewijzing aan de aanwijzer zichtbaar is, kan niet worden aangenomen dat de compiler de volledige programmacoptimalisatie uitvoert (gcc -O2 niet), maar van de toewijzing aan de aanroep wordt de globale -variabele niet opnieuw ingesteld.
toegevoegd de auteur David Rodríguez - dribeas, de bron