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; |
__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) |
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;
} |
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;
} |
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.