wskaźnik vs referencja - wydajność

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.

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.

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.

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?

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
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.

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).

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!

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;
	}
0

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

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.

0

Bo skopałeś testy:

  1. Test1 wywołuje DoSomething2 zaś Test2 wywołuje DoSomething1.
  2. Nie uwzględniłeś że przy starcie masz bardzo nierównomierne przydzieleni czasu procesora, wywołuj w pętle na przemian.

Wersja przekazująca przez parametr (nie ważne poprzez referencje to zrobisz czy poprzez wskaźnik) jest wolniejsza ale bez rewelacji.
Przy tak długiej procedurze jakas_metoda() nie powinno być żadnej odczuwalnej różnicy.

0
n0name_l napisał(a):

#Podawaj nazwę kompilatora, system operacyjny oraz polecenie jakim kompilujesz.

VS 2013 express (+ Community), system to Win7, kompiluję F5.
Co ciekawe ogromne różnice powstają również, gdy zmienię kompilację z x86 na x64!

n0name_l napisał(a):

#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.

U mnie na x64 jest Full Optimization (/Ox) i wyniki wynoszą:
wynik1 = 0.403 -> wskaźnik
wynik2 = 0.141 -> referencja

Na x86 wyniki kształtują się:
wynik1 = 0.207 -> wskaźnik
wynik2 = 1.968 -> referencja

Wydaje mi się, że na x64 duże znaczenie miała wielkość tablicy w klasie A.

Nie mniej jednak różnic wg mnie są ogromne i zależą od wielu czynników!

n0name_l napisał(a):

#http://gcc.godbolt.org/ baw się do woli (wklejasz kod, robisz diffa między outputem funkcji w asmie).

Dzięki, pobawię się ;-)

n0name_l napisał(a):

#Porównywanie tych kodów jako porównywanie przekazywania przez wskaźnik/referencję jest bez sensu.

No, ale przedstawione przeze mnie wyniki dowodzą, że różnice potrafią być ogromne!
A dla mnie to jest o tyle istotne, że właśnie siedzę nad projektem, który mieli GB-ajty danych i ma to dla mnie jakieś znaczenie ;)

_13th_Dragon napisał(a):

Bo skopałeś testy:

Test1 wywołuje DoSomething2 zaś Test2 wywołuje DoSomething1.

Zwróciłem na to uwagę ;-)

_13th_Dragon napisał(a):

Nie uwzględniłeś że przy starcie masz bardzo nierównomierne przydzieleni czasu procesora, wywołuj w pętle na przemian.

Również analizowałem zmianę kolejności wywoływania funkcji Test1 i Test2

_13th_Dragon napisał(a):

Wersja przekazująca przez parametr (nie ważne poprzez referencje to zrobisz czy poprzez wskaźnik) jest wolniejsza ale bez rewelacji.
Przy tak długiej procedurze jakas_metoda() nie powinno być żadnej odczuwalnej różnicy.

A jednak. Moim zdaniem wyniki są znaczące.

Jeszcze raz dziękuję wszystkim za udział w dyskusji!

0
Pijany Samiec napisał(a):
_13th_Dragon napisał(a):

Wersja przekazująca przez parametr (nie ważne poprzez referencje to zrobisz czy poprzez wskaźnik) jest wolniejsza ale bez rewelacji.
Przy tak długiej procedurze jakas_metoda() nie powinno być żadnej odczuwalnej różnicy.

A jednak. Moim zdaniem wyniki są znaczące.

To absolutnie nic nie znaczące wyniki.
Podziel deklaracje klas na .h i .cpp a wyniki się zmienią kardynalnie.

1

Porównujesz dwa nierównoważne kody (już to chyba wspomniano).
Kosz przekazania parametru jest niezerowy.
W jednej wersji go przekazujesz za każdym wywołaniem iteracji, w drugim nie.

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