2012-09-19 15 views
23

Napisałem klasę wektorową 3D, używając wielu nieskomplikowanych kompilatorów SSE. Wszystko działało dobrze, dopóki nie zacząłem inicjować zajęć, które mają wektor 3D jako nowy. Doświadczyłem nieparzystych awarii w trybie zwolnienia, ale nie w trybie debugowania i na odwrót.SSE, wewnętrzne i wyrównanie

Więc czytałem kilka artykułów i doszedłem do wniosku, że muszę wyrównać klasy będące własnością instancji klasy wektorowej 3D do 16 bajtów. Więc po prostu dodaje _MM_ALIGN16 (__declspec(align(16)) przed klasami tak:

_MM_ALIGN16 struct Sphere 
{ 
    // .... 

    Vector3 point; 
    float radius 
}; 

To wydawało się rozwiązać ten problem na początku. Ale po zmianie jakiegoś kodu mój program ponownie zaczął się dziwnie załamywać. Przeszukałem sieć jeszcze bardziej i znalazłem artykuł o blog. Spróbowałem tego, co autor, Ernst Hot, zrobił, aby rozwiązać problem i działa również dla mnie. Dodałem nowe i usunąć operatorów do moich zajęć tak:

_MM_ALIGN16 struct Sphere 
{ 
    // .... 

    void *operator new (unsigned int size) 
    { return _mm_malloc(size, 16); } 

    void operator delete (void *p) 
    { _mm_free(p); } 

    Vector3 point; 
    float radius 
}; 

Ernst wspomina, że ​​ten aproach może być problematyczne, jak również, ale on po prostu linki do forum, które już nie istnieje, nie wyjaśniając, dlaczego to może być problematyczne.

Więc moje pytania to:

  1. czym problem z określeniem operatorów?

  2. Dlaczego dodawanie _MM_ALIGN16 do wystarczającej definicji klasy?

  3. Jaki jest najlepszy sposób poradzenia sobie z problemami związanymi z dopasowaniem pochodzącymi z samoistną SSE?

+0

Czy w pierwszym przypadku przydzielacie swoje konstrukcje do stosu lub sterty? Nie jestem pewien, czy malloc wraca domyślnie do pamięci wyrównanej, podczas gdy _mm_malloc na pewno by - co masz na myśli mówiąc "po jakimś czasie mój program znowu zaczął się zawieszać"? Masz na myśli, że po opuszczeniu go działa na trochę (i co to było)? – Thomas

+0

Problemy zaczęły się, gdy zacząłem przydzielać struktury na stercie. W zdaniu "po chwili" mam na myśli to, że zaczął się on zawiesza po zmianie kodu. Sądzę, że wyrównanie było słuszne przez przypadek, a potem je zniszczyłem. Myślę, że malloc nie zwraca 16 bajtów wyrównanych, co jest problemem, jak sądzę. Moje pytanie brzmi tak naprawdę, jaki jest problem z podejściem operatora i jaki jest najlepszy sposób zarządzania kodem za pomocą wewnętrznej samoistności SSE. –

+2

W rzeczywistości nie trzeba określać wyrównania 'Sphere' (używając tej rzeczy' _MM_ALIGN16'), ponieważ kompilator jest wystarczająco inteligentny, aby zobaczyć, że 'Sphere' ma element wyrównany do 16 i automatycznie dostosowuje' Sphere' ' s wymagania wyrównania (biorąc pod uwagę, że "Vector3" jest prawidłowo wyrównany). Właśnie dlatego nie musisz jawnie wyrównywać "Vector3", jeśli ma już element '__m128'. To tylko alokacja dynamiczna jest problemem i można to przezwyciężyć przez przeciążenie 'operator new/delete', jak napisano na blogu (i zwykle dodatkowe rzeczy, takie jak specjalizacja' std :: allocator'). –

Odpowiedz

18

Przede wszystkim trzeba dbać o dwóch rodzajach alokacji pamięci:

  • statyczny przydział. Aby zmienne automatyczne były prawidłowo wyrównane, Twój typ wymaga odpowiedniej specyfikacji wyrównania (np. __declspec(align(16)), __attribute__((aligned(16))) lub Twojego _MM_ALIGN16). Ale na szczęście potrzebujesz tego tylko wtedy, gdy wymagania wyrównania podane przez członków typu (jeśli są) nie są wystarczające. Więc nie potrzebujesz tego dla ciebie Sphere, ponieważ twój Vector3 jest już odpowiednio wyrównany. A jeśli twój Vector3 zawiera element __m128 (co jest całkiem prawdopodobne, w przeciwnym razie sugerowałbym to zrobić), to nie potrzebujesz go nawet dla Vector3. Więc zwykle nie musisz mieszać z atrybutami wyrównania specyficznymi dla kompilatora.

  • Alokacja dynamiczna. Tyle o łatwej części. Problem polega na tym, że C++ używa na najniższym poziomie raczej agnostycznej funkcji alokacji pamięci do alokowania dowolnej pamięci dynamicznej. Zapewnia to tylko prawidłowe wyrównanie dla wszystkich standardowych typów, które mogą mieć 16 bajtów, ale nie jest gwarantowane.

    Aby to zrekompensować, musisz przeciążać wbudowane operator new/delete, aby zaimplementować własną alokację pamięci i użyć dopasowanej funkcji alokacji pod maską zamiast starego dobrego malloc. Przeciążanie operator new/delete to temat sam w sobie, ale nie jest to tak trudne, jak mogłoby się wydawać na początku (chociaż twój przykład nie wystarcza) i możesz przeczytać o nim w this excellent FAQ question.

    Niestety, musisz to zrobić dla każdego typu, który ma dowolnego członka wymagającego niestandardowego wyrównania, w twoim przypadku zarówno Sphere i Vector3. Ale możesz zrobić to nieco łatwiej, po prostu stwórz pustą klasę bazową z odpowiednimi przeciążeniami dla tych operatorów, a następnie wyprowadź wszystkie potrzebne klasy z tej klasy bazowej.

    Większość ludzi czasami zapominają, że średnia podzielnik std::alocator wykorzystuje globalny operator new dla całej alokacji pamięci, dzięki czemu typy nie będą pracować ze standardowymi pojemnikami (a std::vector<Vector3> nie jest to rzadki przypadek użycia). Musisz zrobić swój własny standardowy alokator zgodności i użyć go. Jednak dla wygody i bezpieczeństwa lepiej jest po prostu specjalizować się w typie std::allocator (może po prostu wyprowadzić go z niestandardowego przydziału), tak aby był zawsze używany i nie musisz dbać o to, aby użyć właściwego przydziału za każdym razem, gdy używasz std::vector. Niestety w tym przypadku musisz ponownie specjalizować go dla każdego wyrównanego typu, ale pomaga w tym małe zło makro.

    Dodatkowo musisz zwracać uwagę na inne rzeczy, korzystając z globalnej wersji operator new/delete zamiast niestandardowej, np. std::get_temporary_buffer i std::return_temporary_buffer, i zadbać o tych, jeśli to konieczne.

Niestety nie ma jeszcze znacznie lepsze podejście do tych problemów, myślę, chyba że jesteś na platformie, która natywnie dostosowuje do 16 i wiedzą o tym. Lub możesz po prostu przeciążać globalne operator new/delete, aby zawsze wyrównać każdy blok pamięci do 16 bajtów i być wolnym od troski o wyrównanie każdej klasy zawierającej element SSE, ale nie wiem o konsekwencjach tego podejścia. W najgorszym przypadku powinno to po prostu spowodować marnowanie pamięci, ale z drugiej strony zazwyczaj nie przypisuje się dynamicznie małych obiektów w C++ (chociaż std::list i std::map mogą inaczej o tym myśleć).

Więc Podsumowując:

  • Opieka nad właściwym ułożeniu pamięci statycznej przy użyciu rzeczy jak __declspec(align(16)), ale tylko wtedy, gdy nie jest już pod opieką każdego członka, który jest zwykle przypadek.

  • Przeciążenie operator new/delete dla każdego typu mającego element o niestandardowych wymaganiach wyrównania.

  • Utwórz standardowy identyfikator zgodny ze standardem cunstom, który będzie używany w standardowych kontenerach z wyrównanymi typami lub jeszcze lepiej wyspecjalizowany std::allocator dla każdego wyrównanego typu.


Wreszcie niektóre ogólne porady. Często zyskujesz tylko na SSE w ciężkich blokach obliczeniowych podczas wykonywania wielu operacji wektorowych. Aby uprościć wszystkie te problemy z wyrównaniem, w szczególności problemy związane z dopasowaniem każdego typu zawierającego Vector3, może być dobrym podejściem do stworzenia specjalnego wektora wektorowego SSE i używać go tylko w długich obliczeniach, używając normalnego -SSE wektor do przechowywania i zmiennych członkowskich.

+0

Czy "std :: aligned_storage" z C++ 11 umożliwiałoby to wszystko bez potrzeby stosowania specjalistycznych konwencji wywoływania? –

+1

@ graham.reeds Zamiast słowa kluczowego "alignas". 'std :: aligned_storage' nie jest tak naprawdę potrzebny, biorąc pod uwagę, że' __m128' jest już poprawnie wyrównany i wolałbyś raczej mieć człon "__m128" zamiast 'std :: aligned_storage'. Ale na pewno "alignas" jest nowym niezależnym od platformy sposobem wymawiania '__declspec (align())' (lub podobnego gcc), nawet jeśli żaden z nich nie jest w ogóle potrzebny. Wszystko to jednak pomaga w wyrównaniu pamięci statycznej. –

1
  1. Problem z operatorów jest to, że same nie są one wystarczające. Nie wpływają na alokacje stosów, dla których nadal potrzebujesz __declspec(align(16)).

  2. __declspec(align(16)) wpływa na sposób, w jaki kompilator umieszcza obiekty w pamięci, wtedy i tylko wtedy, gdy ma do wyboru. W przypadku obiektów new'ed kompilator nie ma innego wyjścia, jak użyć pamięci zwróconej przez operator new.

  3. Najlepiej użyć kompilatora, który obsługuje je natywnie. Nie ma teoretycznego powodu, dla którego należy je traktować inaczej niż double. W przeciwnym razie przeczytaj dokumentację kompilatora, aby obejść ten problem. Każdy niepełnosprawny kompilator będzie miał własny zestaw problemów, a więc własny zestaw obejść.

+0

Dzięki! Z komentarza Christiana Rau wynika, że ​​'__declspec (align (16)) jest przestarzały. Chyba ta część zależy od kompilatora? Nie jestem pewien, czy rozumiem 3. część twojej odpowiedzi. Co rozumiesz przez "radzenie sobie z nimi". Używam kompilatora dołączonego do programu Visual Studio 2012 Express. –

+1

@FairDinkumThinkum: Co mam na myśli przez "kompilator, który obsługuje je natywnie" to kompilator, który może wyrównywać typy SSE tak, jak wyrównuje typy FP, tj. Bez pomocy programisty. Nie potrzebujesz do tego '__declspec (align (8)). Nie mam VS2012, więc nie mogę powiedzieć na pewno, czy to już jest tak inteligentne. – MSalters

+1

Najprawdopodobniej musisz zaimplementować niestandardowy przydział również. –

2

Zasadniczo należy upewnić się, że wektory są prawidłowo wyrównane, ponieważ typy wektorów SIMD mają zwykle większe wymagania dotyczące wyrównania niż jakiekolwiek wbudowane typy.

które wymaga robić następujące rzeczy:

  1. Upewnij się, że Vector3 jest prawidłowo ustawiony, gdy jest na stosie lub członka struktury. Odbywa się to przez zastosowanie klasy __attribute__((aligned(32))) do Vector3 (lub dowolnego atrybutu obsługiwanego przez kompilator). Zauważ, że nie musisz stosować atrybutu do struktur zawierających Vector3, co nie jest konieczne i niewystarczające (tj.nie trzeba go stosować do Sphere).

  2. Upewnij się, że Vector3 lub jego struktura otaczająca jest prawidłowo wyrównana podczas korzystania z alokacji sterty. Odbywa się to przez użycie posix_memalign() (lub podobnej funkcji dla twojej platformy) zamiast zwykłego malloc() lub operator new(), ponieważ te ostatnie dwa wyrównują pamięć dla wbudowanych typów (zwykle 8 lub 16 bajtów), co nie jest gwarantowane dla typów SIMD .