czwartek, 3 września 2009

Konstruktory, konstruktory

Załóżmy, że mamy klasę, w której nie ma funkcji wirtualnych (!), po niczym nie dziedziczy (w szczególności wirtualnie), i skomponowana jest tylko z niestatycznych i niestałych obiektów podstawowych. Jest to klasyczny przypadek klas typu Vector2, Vector3, Matrix3x3, Matrix4x4, oraz wielu innych matematycznych (i nie tylko) w grach. W takich klasach bardzo często wywoływany jest konstruktor kopiujący, więc warto byłoby się zastanowić jak taki konstruktor napisać optymalnie (*). Najprostszy sposób, to kopiowanie każdej wartości osobno. Inną możliwością jest wywołanie funkcji w rodzaju memcpy, żeby cały blok danych przekopiować na raz (stąd założenie o funkcjach wirtualnych etc., żeby nie nadpisać vtable pointera), co z kolei powoduje nakład na całą zabawę z ramką stosu. Teoretycznie można to jeszcze zoptymalizować przez modyfikator inline dla konstruktora. Ostatnie podejście, które sprawdziłem to... nie pisanie konstruktora ;P
Do testu przygotowałem 4 klasy - A, B, C i D, wykorzystujące wspomniane 4 podejścia. Konstruktory kopiujące dla A, B i C wyglądają następująco (dla testu drugiego, czyli symulacji macierzy 4x4, test pierwszy odpowiadał wektorowi trójelementowemu):
A(const A& a) {
x1 = a.x1; y1 = a.y1; z1 = a.z1; w1 = a.w1;
x2 = a.x2; y2 = a.y2; z2 = a.z2; w2 = a.w2;
x3 = a.x3; y3 = a.y3; z3 = a.z3; w3 = a.w3;
x4 = a.x4; y4 = a.y4; z4 = a.z4; w4 = a.w4;
}

B(const B& b) {
memcpy(this, &b, sizeof(B));
}

inline C(const C& c) {
memcpy(this, &c, sizeof(C));
}
Następnie dla każdej z 4 klas przeprowadziłem identyczny test ( testCycles = 100000000):
for(int i = 0; i < testCycles; i++) {
A acopy = A(a);
}
Jak wspomniałem, test 1 odpowiadał Vector3, test 2 - Matrix4x4.

Można się było domyślać, że narzut wywołania funkcji przewyższy zysk przy małej liczbie elementów do skopiowania. Natomiast wynik dla klasy D, z wygenerowanym automatycznie konstruktorem kopiującym jest dosyć zastanawiający, szczególnie dla przypadku 1 (aż z wrażenia dla pewności sprawdziłem, czy kompilator w ogóle kopiował obiekty :P). Więc moja odpowiedź na pytanie (*) - jak optymalnie napisać taki konstruktor brzmi: nie pisać wcale! Kompilator sobie poradzi.

Przy okazji sprawdziłem jeszcze jedną rzecz. Wielu programistów twierdzi, że lista inicjatorów konstruktora to tylko zabieg stylistyczny i nie różni się od przypisania wartości do obiektu wewnątrz konstruktora. Oczywiście mylnie, bo stosując drugie podejście wywołujemy niejawnie konstruktor obiektu tymczasowego, potem operator przypisania, a potem jeszcze destruktor. Prosty test:
class X {
std::string text;
public:
X() {
text = "text";
}
};

class Y {
std::string text;
public:
Y() : text("text") {}
};
W pętli wywoływałem tym razem zwykły konstruktor, wynik potwierdza to, co napisałem:


2 komentarze:

  1. A jak na tle powyższych czterech wypada kopiowanie z użyciem SSE?

    OdpowiedzUsuń
  2. Dobre pytanie, w wolnej chwili w przypływie nudy sprawdzę i napiszę ;)

    OdpowiedzUsuń