Niektóre funkcje dobrze jest pisać w asemblerze. Tak, wiem że dzisiaj – w epoce języków (zbyt) wysokiego poziomu – brzmi to dziwnie, ale to prawda. To najprostszy sposób na poprawienie wydajności często wykonywanych operacji, np. kalkulacji z użyciem wektorów i macierzy.
Rzecz w tym, że korzystając bezpośrednio z zaawansowanych możliwości oferowanych przez współczesne procesory, jednocześnie uzależniamy się od nich. Przykładowo, transformację wektora przez macierz można naturalnie po prostu przetłumaczyć z odpowiedniego wzoru na instrukcje jednostki zmiennoprzecinkowej i uzyskać kod działający na każdym procesorze. Jeżeli jednak użyjemy np. SSE2, możemy uzyskać kilkakrotny wzrost wydajności – lecz wówczas nasza funkcja będzie działała tylko na nowszych procesorach.
Najlepiej byłoby więc mieć kilka wersji takiej funkcji i wybierać odpowiednią dla procesora pracującego na danej maszynie. Jak jednak wykryć, co potrafi dana jednostka? Otóż z pomocą przychodzi nam system operacyjny. W Windows na przykład istnieje funkcja o wiele mówiącej nazwie IsProcessorFeaturePresent
, przy pomocy której możemy sprawdzić obecność rozszerzeń MMX, 3DNow!, SSE i SSE2.
Oczywiście, takiego sprawdzenia należy dokonać raz na początku działania programu. Jeśli jednak po prostu zapiszemy jego rezultat w formie globalnych flag boolowskich, to ich odczytywanie np. przy każdym dodawaniu wektorów będzie nie tylko kłopotliwe, ale i nieefektywne.
Lepszym rozwiązaniem jest stworzenie odpowiedniej liczby globalnych wskaźników na funkcje, inicjowanych w czasie uruchamiania programu; tak jak poniżej:
Dzięki temu zarówno w kodzie asemblerowym poszczególnych wersji (którego litościwie nie pokażę ;D), jak i wywołaniach, nie widać żadnego śladu po ‘magii’ wyboru funkcji dostosowanej do procesora. Narzut to rozwiązanie to naturalnie jedna dereferencja wskaźnika więcej; sprawdzanie flag (porównaniami i skokami) trwałoby znacznie dłużej.
Hej :)
Właśnie niedawno zastanawiałem się nad zrobieniem podobnego tricku, ale do wyboru rodzaju potencjału oddziaływania pomiędzy cząsteczkami w symulacji komputerowej, którą robię. Jednak zrezygnowałem z uniwersalności na rzecz brzydszego kodu z prostego powodu: wydajność. Kompilatory w dzisiejszych czasach wiele funkcji rozwijają inline, co daje o niebo lepszą wydajność w wielu przypadkach. W moim przypadku program przyspieszał 3 razy (wywoływana funkcja była dość newralgicznym miejscem :) ). Myślę, że także dla Was (zakładam, że większość czytelników i sam autor to koderzy gier) wydajność ma duże znaczenie. Niestety również przykładowe obliczenia na wektorach są dość często wykonywanymi operacjami, więc pewnie czasem by się opłacało je rozwinąć inline, co jest niemożliwe gdy korzystamy ze wskaźników na funkcje – z prostej przyczyny: wybór wywoływanej funkcji następuje w czasie pracy programu, a nie jego kompilacji.
Z tego co wiem, to niektórzy raczej wolą napisać po kilka razy kod tak, aby uwzględnić różne procesorowe scenariusze. Innym rozwiązaniem są dyrektywy, ale wiadomo… trzeba wtedy kompilować kod na różne maszyny, albo udostępnić kod użytkownikom.
Można też stosować te wskaźniki, ale ustawiać je dla funkcji “wyższego poziomu”, przykładowo nie na obliczenie iloczynu macierzy, ale np. na wykonanie serii takich operacji. I wtedy przygotować po prostu kilka wersji tych funkcji, które są rzadziej wykonywane i których czas wywołania jest nieporównywalnie krótszy niż czas potrzebny na zakończenie tej funkcji. Chyba trochę zakręciłem ;).
Hm, jeszcze jeden pomysł: robisz funkcję Vec3_Add i w niej zwykłym if`em decydujesz o tym, która funkcja ma się wykonać. Wtedy te funkcje mogą być rozwinięte inline.
Pozostaje jednak pytanie: kiedy opłaca się kombinować? Z mojego doświadczenia wynika, że różne zabiegi na różnych procesorach i kompilatorach dają różne wyniki. Przewidzenie rozwiązania optymalnego wymagałoby zgłębienia tajników procesorów i kompilatorów na których się pracuje :). Ja na razie na to czasu nie mam, więc pozostaje mi intuicja i eksperymenty.
pozdrawiam
Niektórzy po prostu olewają procki niemające np. SSE2. Też jakieś rozwiązanie :)
I w sumie nawet nie takie złe, bo SSE2 jest w prockach Intela począwszy od P4. Zdecydowanie gorzej jest jednak z AMD.
io: Przykład z wektorami jest głównie przykładem właśnie, bo dla nich faktycznie może opłacić się rozwinąć zwykły kod inline. Ale już np. przy mnożeniu macierzy można zejść z liczbą instrukcji nawet kilkunastokrotnie, więc nawet kładzenie argumentów na stos i skok nie muszą tego zysku zepsuć.
Mala dygresja: z tego co sie orientuje, to D3DX jest na tyle mily, ze juz to robi :)
Ostatecznie mozna zrobic 2 kompilacje :))