wskaźnik vs referencja - wydajność

Odpowiedz Nowy wątek
2015-01-17 20:51
Pijany Samiec
0

Hey, nurtuje mnie takie pytanie:
czy w poniższym przykładzie jedna metoda może wykonywać się nieznacznie szybciej od drugiej z powodu, że do jednej przekazujemy obiekt jako referencję a w drugiej korzystamy ze wskaźnika?

class A
    {
    public:
    void jakas_metoda(){}
    //...
    };

class B
    {
    void DoSomething1(A& a)
        {
        a.jakas_metoda();
        //...
        }

    void DoSomething2()
        {
        a->jakas_metoda();
        //...
        }

    A* a;
    };

Z góry dzięki za rozwianie wątpliwości.

Pozostało 580 znaków

2015-01-17 21:05
0

Nie powinno być różnicy. A na pewno nie na tyle, by się tym przejmować. Referencje są wygodniejsze, więc zawsze ich użyj jak masz 2 opcje do wyboru.
Kiedyś mi się obiło o uszy, że referencje są implementowane jako wskaźniki, ale źródła nie pamiętam, więc głowy nie dam.

Pozostało 580 znaków

2015-01-17 21:13
0

W przypadku klas nie ma najmniejszej różnicy. Referencja zachowuje się identycznie jak wskaźnik. Inaczej jest w przypadku wartości liczbowych, które mieszczą się na rejestrach procesora. W takim przypadku referencja będzie bezpośrednim operowaniem na zmiennej. Wskaźnik będzie drogą naokoło i czystą głupotą.

Osobiście wolę wskaźniki dlatego, że mogą nigdzie nie wskazywać (=NULL) i mogą być zmienione w funkcji.


Pozostało 580 znaków

2015-01-17 21:17
Pijany Samiec
0
twonek napisał(a):

Nie powinno być różnicy. A na pewno nie na tyle, by się tym przejmować. Referencje są wygodniejsze, więc zawsze ich użyj jak masz 2 opcje do wyboru.
Kiedyś mi się obiło o uszy, że referencje są implementowane jako wskaźniki, ale źródła nie pamiętam, więc głowy nie dam.

Nie to, żebym się przejmował, ale nurtuje mnie to trochę ;-).
To, że referencje przeważnie są wygodniejsze to wiem i również tam gdzie mogę to je stosuję.

Też wydaje mi się, że referencja powinna być traktowana jak wskaźnik (bo jak inaczej?), ale gdy przekazując do funkcji zmienną, która jest referencją możemy od razu przypisać jej wartość tj. np.

void fun(int& a)
{
a = 3;
}

Natomiast przesyłając wskaźnik musimy wyłuskać najpierw tą zmienną tj.

void fun(int*& a)
{
*a = 3;
}

A zdaje się, że takie wyłuskanie już jest kosztowne (minimalnie ofc) - tak przynajmniej wynikało z testów, które kiedyś robiłem.

Z drugiej strony, w podanym przeze mnie wyżej przykładzie, nie ma wyłuskiwania (przynajmniej tak mi się wydaje, bo nie wiem jak interpretować operator "->").

No i teraz pytanie do osób, które lepiej ogarniają to co się dzieje na niskim poziomie (asembler/kompilator) co one na to?

Pozostało 580 znaków

2015-01-17 21:24
0

Wszystko zależy od wartości RunCount:

const unsigned RunCount=1000;
void Test1(A &a,B &b) { b->a=&a; for(unsigned i=0;i<RunCount;++i) b->DoSomething2(); }
void Test2(A &a,B &b) { for(unsigned i=0;i<RunCount;++i) b->DoSomething1(a); }
  1. Dla wartości RunCount>2 na 100% wydajniejszy będzie Test1
  2. Dla wartości RunCount==1 na 100% wydajniejszy będzie Test2
  3. Dla wartości RunCount==2 wg mnie zależy od sprzętu

Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
edytowany 1x, ostatnio: _13th_Dragon, 2015-01-18 15:56

Pozostało 580 znaków

2015-01-17 21:26
2
Pijany Samiec napisał(a):

Nie to, żebym się przejmował, ale nurtuje mnie to trochę ;-).
To, że referencje przeważnie są wygodniejsze to wiem i również tam gdzie mogę to je stosuję.
Też wydaje mi się, że referencja powinna być traktowana jak wskaźnik (bo jak inaczej?), ale gdy przekazując do funkcji zmienną, która jest referencją możemy od razu przypisać jej wartość tj. np.
(...)
A zdaje się, że takie wyłuskanie już jest kosztowne (minimalnie ofc) - tak przynajmniej wynikało z testów, które kiedyś robiłem.
Z drugiej strony, w podanym przeze mnie wyżej przykładzie, nie ma wyłuskiwania (przynajmniej tak mi się wydaje, bo nie wiem jak interpretować operator "->").
No i teraz pytanie do osób, które lepiej ogarniają to co się dzieje na niskim poziomie (asembler/kompilator) co one na to?

Nie będzie absolutnie żadnej różnicy w "wydajności". Każdy kompilator C++ implementuje referencje za pomocą wskaźników. Różnica polega w sposobie w jaki używa się tych tworów na poziomie języka.

Nie przejmuj się takimi głupotam. Po prostu pisz kod.


"(...) otherwise, the behavior is undefined".
edytowany 2x, ostatnio: Endrju, 2015-01-17 21:29

Pozostało 580 znaków

2015-01-17 21:32
6

Odpowiedź na pytanie: Nie ma żadnej różnicy, a tymbardziej żadnej, która powinna cię obchodzić z punktu widzenia programisty.

Co robią implementacje, spójrzmy (Windows7/MinGW, inne zachowują się tak samo/podobnie).
Przykładowy kodzik:

#include <iostream>
using namespace std;

void byref(int& ref) {
    cout << ref << endl;
}

void byptr(int* ptr) {
    cout << *ptr << endl;
}

int main(int argc, char** argv) {
    int a{0};
    byref(a);
    byptr(&a);
    return 0;
}

Dość prosty, tworzymy zmienną, dalej ją przekazujemy do funkcji przez referencje i przez wskaźnik.

Teraz kompilacja:
g++ test.cpp -o test.exe -std=c++11 -fdump-tree-all

Opcja -fdump-tree-all zrzuci nam poszczególne etapy kompilacji w różnych plikach, nas interesuje ten z rozszerzeniem optimized czyli kod już w miarę "gotowy".
Jak to wygląda:

;; Function void byref(int&) (_Z5byrefRi, funcdef_no=1137, decl_uid=25418, cgraph_uid=234)

void byref(int&) (int & ref)
{
  struct basic_ostream & D.25570;
  struct basic_ostream & D.25569;
  int D.25568;
  int _2;
  struct basic_ostream & _3;
  struct basic_ostream & _4;

  <bb 2>:
  _2 = *ref_1(D);
  _3 = std::basic_ostream<char>::operator<< (&cout, _2);
  _4 = _3;
  std::basic_ostream<char>::operator<< (_4, endl);
  return;

}

;; Function void byptr(int*) (_Z5byptrPi, funcdef_no=1138, decl_uid=25424, cgraph_uid=235)

void byptr(int*) (int * ptr)
{
  struct basic_ostream & D.25615;
  struct basic_ostream & D.25614;
  int D.25613;
  int _2;
  struct basic_ostream & _3;
  struct basic_ostream & _4;

  <bb 2>:
  _2 = *ptr_1(D);
  _3 = std::basic_ostream<char>::operator<< (&cout, _2);
  _4 = _3;
  std::basic_ostream<char>::operator<< (_4, endl);
  return;

}

;; Function int main(int, char**) (main, funcdef_no=1139, decl_uid=25428, cgraph_uid=236)

int main(int, char**) (int argc, char * * argv)
{
  void * D.25643;
  int a;
  int D.25616;
  int _1;

  <bb 2>:
  a = 0;
  byref (&a);

  <bb 3>:
  byptr (&a);

  <bb 4>:
  _1 = 0;
  a ={v} {CLOBBER};

<L1>:
  return _1;

<L2>:
  _2 = __builtin_eh_pointer (1);
  __builtin_unwind_resume (_2);

}

;; Function void __tcf_0() (__tcf_0, funcdef_no=1164, decl_uid=25563, cgraph_uid=260)

void __tcf_0() ()
{
  <bb 2>:
  std::ios_base::Init::~Init (&__ioinit);
  return;

}

;; Function void __static_initialization_and_destruction_0(int, int) (_Z41__static_initialization_and_destruction_0ii, funcdef_no=1163, decl_uid=25559, cgraph_uid=261)

void __static_initialization_and_destruction_0(int, int) (int __initialize_p, int __priority)
{
  <bb 2>:
  if (__initialize_p_1(D) == 1)
    goto <bb 3>;
  else
    goto <bb 5>;

  <bb 3>:
  if (__priority_2(D) == 65535)
    goto <bb 4>;
  else
    goto <bb 5>;

  <bb 4>:
  std::ios_base::Init::Init (&__ioinit);
  atexit (__tcf_0);

  <bb 5>:
  return;

}

;; Function (static initializers for test.cpp) (_GLOBAL__sub_I__Z5byrefRi, funcdef_no=1165, decl_uid=25566, cgraph_uid=262)

(static initializers for test.cpp) ()
{
  <bb 2>:
  __static_initialization_and_destruction_0 (1, 65535);
  return;

}

Na co zwrócić uwagę:
#Obie funkcji mają takie same ciała.
#Wywołania obu funkcji wyglądają identycznie.

Dalej, możemy sobie odpalić jakis disassembler i zobaczyć jak wygląda już skompilowane w pełni wywołanie funkcji. A no, identycznie w obu przypadkach.

mov     [esp+24h+var_24], ebx
call    sub_4013F0
mov     [esp+24h+var_24], ebx
call    sub_401470

Jak ktoś jeszcze nie dowierza to zachęcam do sprawdzenia implementacji tych funkcji (przez użycie cout są one dość długie w asmie).

Żeby taki eksperyment wyglądał tysiąc razy czytelniej po prostu dodaj void sink(int); i użyj zamiast cout. Kod się nie zlinkuje, ale kompilacja będzie ok. - Endrju 2015-01-17 21:43
O, dzięki! Nie pomyślałem o tym. :P - n0name_l 2015-01-17 23:05

Pozostało 580 znaków

2015-01-17 21:44
Pijany Samiec
0

Ok! Dziękuję Wam bardzo za rozwianie moich wątpliwości ;-).
W 100% zgadzam się z waszymi uwagami!
Temat uważam za zamknięty!
pozdrawiam!

Pozostało 580 znaków

2015-01-17 22:35
Pijany Samiec
0

A jednak chyba jest coś na rzeczy...
Troszkę zmodyfikowałem test @_13th_Dragon i wychodzi, że referencja rzeczywiście jest szybsza...
Ale dlaczego?!

Poniżej kod testu:

class A
    {
    public:
        A() 
            {
            for(unsigned i = 0; i < n; ++i)
                tab[i] = 0;
            }
        void jakas_metoda()
            {
            for(unsigned i = 0; i < n; ++i)
                tab[i] += 2;
            }
        //...
        static const unsigned n = 100;
        long long unsigned tab[n];
    };

class B
    {
    public:
    void DoSomething1(A& a)
        {
        a.jakas_metoda();
        //...
        }

    void DoSomething2()
        {
        a->jakas_metoda();
        //...
        }

    A* a;
    };

const long long unsigned RunCount = 10000000;
void Test1(A &a, B &b) 
    {
    b.a = &a; 
    for(unsigned i = 0; i<RunCount; ++i) 
        b.DoSomething2();
    }
void Test2(A &a, B &b) 
    {
    for(unsigned i = 0; i<RunCount; ++i) 
        b.DoSomething1(a);
    }

using namespace std;
int _tmain(int argc, _TCHAR* argv[])
    {
    clock_t start, stop;
    A a;
    B b;

    start = clock();
    Test1(a, b);
    stop = clock();
    cout << "wynik1 = " << (double)(stop - start) / CLOCKS_PER_SEC << endl;

    start = clock();
    Test2(a, b);
    stop = clock();
    cout << "wynik2 = " << (double)(stop - start) / CLOCKS_PER_SEC << endl;

    getchar();
    return 0;
    }

Pozostało 580 znaków

2015-01-17 22:36
Pijany Samiec
0

Aha, i to nie są małe różnice -> referencja jest jakieś 3 razy szybsza!

Pozostało 580 znaków

2015-01-17 23:04
0

#Podawaj nazwę kompilatora, system operacyjny oraz polecenie jakim kompilujesz.
#Różnica (z wyłączoną optymalizacją ofc.) jest minimalna (Windows7/MinGW), tj. 0,0XY dla podanych wartości.
#Z włączoną optymalizacją (-O2) oba wyniki to 0.
#http://gcc.godbolt.org/ baw się do woli (wklejasz kod, robisz diffa między outputem funkcji w asmie).
#Porównywanie tych kodów jako porównywanie przekazywania przez wskaźnik/referencję jest bez sensu.

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0