Wyrównanie pamięci dla operatorów new i new[]
Gdy programuje się coś z użyciem funkcji wewnętrznych kompilatora (Intrinsics) dla SSE, najlepiej jest gdy to, co do nich przekazujemy jest wyrównanie do 16 bajtów, w przeciwnym wypadku powstaje problem z odczytaniem z pamięci i program się wysypuje. Oczywiście można korzystać z funkcji ładujących, które przyjmują dane niewyrównane, ale funkcje te są z oczywistych względów wolniejsze. Do ustawienia wyrównania danych pól stosuję się specjalną deklaracje __declspec(align(16)), którą należy umieścić przed definicją zmiennej (oczywiście dotyczy to Microsoft Visual Studio).
__declspec(align(16)) __m128 m_vector; |
Niestety taka deklaracja dotyczy tylko obiektów na stosie. Te, które są tworzone na stercie, czyli przez operator new oraz operator new[], nie zawsze są wyrównane, dlatego trzeba zadbać o to samemu odpowiednio przeciążając te funkcje. Przeciążanie tych operatorów nie jest trudne. Oba przyjmują jako argument ilość potrzebnego miejsca wyrażoną w bajtach, a zwracają wskaźnik typu void, który zawiera adres odpowiedniego obszaru pamięci. Nagłówek jednego z tych operatorów wygląda następująco:
void* operator new(const size_t nBytes) |
W ciele takiej funkcji należy zaalokować odpowiednią ilość miejsca, a następnie wyrównać wskaźnik tak, aby dzielił się przez 16. Dopiero ten wskaźnik można zwrócić. Należy oczywiście pamiętać o zapisaniu gdzieś poprzedniego wskaźnika oraz o alokacji pamięci tak, żeby po wyrównaniu nie wychodzić poza przydzielony zakres.
W przypadku mojej funkcji adres jest zapisany przed wyrównanym adresem (dzięki poradom kolegów z forum ), a ilość alokowanego miejsca jest większa o rozmiar alokowanej struktury + 3 bajty.
void* operator new(const size_t nBytes) { char* ptr = new char[nBytes + sizeof(NVector) + 3]; unsigned int temp = reinterpret_cast<unsigned int>(ptr); // Konwersja na postać odpowiednią do obliczeń unsigned int t = temp % 16; // Obliczam o ile jest przesunięty od wyrównania temp += (t < 13) ? (16 - t) : (32 - t); // Dodaje odpowiednie przesunięcie do wskaźnika void* aPtr = reinterpret_cast<void*>(temp); // Zapis nowego wskaźnika temp -= sizeof(void*); *reinterpret_cast<unsigned int*>(temp) = reinterpret_cast<unsigned int>(ptr); // Zapis poprzedniego wskaźnika return aPtr; } |
NVector jest klasą, w której umieściłem ten operator. Liczba 13 występująca przy porównywaniu w powyższym kodzie reprezentuje przesunięcie, dla którego nie jest możliwe zapisanie wskaźnika przed najbliższym wyrównaniem, dlatego trzeba dodać więcej. Operator new[]wygląda identycznie, dlatego można w nim wywołać powyższą funkcję. Odpowiednik delete dla tego operatora wygląda następująco:
void operator delete(void* p) { unsigned int temp = reinterpret_cast<unsigned int>(p) - sizeof(void*); char* temp2 = reinterpret_cast<char*>(*reinterpret_cast<int*>(temp)); delete[] temp2; } |
Powyższa funkcja oblicza poprzedni wskaźnik i korzystając z niego dealokuje obszar. Operator delete[] jest identyczny.
Reg:
Ten __declspec(align(16)) jest tu niepotrzebny. Cytując MSDN Library: “Variables of type _m128 are automatically aligned on 16-byte boundaries.”.
14 December 2008, 1:27 pmNetrix:
Zgadza się, dałem z przezorności :).
14 December 2008, 9:00 pmKhaine:
a o ile szybszy jest ten new od standardowego new, lub od wlasnego managera pamieci?
reinterprect_cast tylko z RTTI :P
25 December 2008, 7:26 pmNetrix:
Zależy o którym new piszesz, placement new, działa na wcześniej zaalokowanej pamięci, a ten który ja napisałem, jest po to, żeby uzyskać wyrównany adres pamięci.
1 January 2009, 7:10 pm