2013-01-18 7 views
7

Zajmuję optymalizacje dla moich obliczeń 3D i teraz mam:mają różne optymalizacje (zwykły, SSE, AVX) W tym samym wykonywalny z C/C++

  • A "plain" wersja użyciu standardowego C biblioteki językowe,
  • SSE zoptymalizowana wersja, która kompiluje używając preprocesora #define USE_SSE,
  • AVX wersja zoptymalizowana że kompiluje za pomocą preprocesora #define USE_AVX

Czy można przełączać się między 3 wersjami bez kompilacji różnych plików wykonywalnych (np. mając różne pliki biblioteki i dynamicznie ładując "prawą", nie wiem, czy funkcje inline są "odpowiednie" do tego)? Zastanowiłabym się również nad występami związanymi z takim przełącznikiem w oprogramowaniu.

+1

Brak wzmianki o platformie? Niektóre platformy odmówią uruchomienia kodu za pomocą avx, nawet jeśli wiesz, że te instrukcje nigdy nie będą wywoływane. Niektóre platformy mają ifunc do wyboru między kilkoma implementacjami w czasie wykonywania. Niektóre platformy szukają bibliotek współdzielonych w ścieżkach zależnych od możliwości. –

Odpowiedz

5

Jednym ze sposobów jest zaimplementowanie trzech bibliotek zgodnych z tym samym interfejsem. W przypadku bibliotek dynamicznych możesz po prostu zamienić plik biblioteki, a plik wykonywalny użyje tego, co znajdzie. Na przykład w systemie Windows, można skompilować trzy DLL:

  • PlainImpl.dll
  • SSEImpl.dll
  • AVXImpl.dll

a następnie dokonać wykonywalny łącza przeciwko Impl.dll. Teraz wystarczy umieścić jedną z trzech określonych bibliotek DLL w tym samym katalogu, co .exe, zmienić nazwę na Impl.dll i będzie korzystać z tej wersji. Ta sama zasada powinna zasadniczo dotyczyć systemu operacyjnego podobnego do systemu UNIX.

Następnym krokiem byłoby, aby załadować biblioteki programowo, który jest prawdopodobnie najbardziej elastyczne, ale to jest OS specyficzny i wymaga trochę więcej pracy (jak otwarcie biblioteki, uzyskanie wskaźników funkcji etc.)

Edycja : Ale oczywiście, możesz po prostu zaimplementować funkcję trzy razy i wybrać jedną w czasie wykonywania, w zależności od ustawienia parametru/pliku konfiguracyjnego itp., Jak wyszczególniono w innych odpowiedziach.

0

Oczywiście, że to możliwe.

Najlepszym sposobem na zrobienie tego jest posiadanie funkcji, które wykonują kompletne zadanie i wybranie spośród nich w czasie wykonywania. To będzie działać, ale nie jest optymalna:

typedef enum 
{ 
    calc_type_invalid = 0, 
    calc_type_plain, 
    calc_type_sse, 
    calc_type_avx, 
    calc_type_max // not a valid value 
} calc_type; 

void do_my_calculation(float const *input, float *output, size_t len, calc_type ct) 
{ 
    float f; 
    size_t i; 

    for (i = 0; i < len; ++i) 
    { 
     switch (ct) 
     { 
      case calc_type_plain: 
       // plain calculation here 
       break; 
      case calc_type_sse: 
       // SSE calculation here 
       break; 
      case calc_type_avx: 
       // AVX calculation here 
       break; 
      default: 
       fprintf(stderr, "internal error, unexpected calc_type %d", ct); 
       exit(1); 
       break 
     } 
    } 
} 

Na każdym przejściu przez pętlę, kod jest wykonanie switch oświadczenie, które jest tuż nad głową. Naprawdę sprytny kompilator teoretycznie może to naprawić, ale lepiej sam to naprawić.

Zamiast tego napisz trzy oddzielne funkcje: jedną dla zwykłego, jedną dla SSE i jedną dla AVX. Następnie zdecyduj, w którym momencie ma działać.

Aby uzyskać punkty bonusowe, w kompilacji "debugowania", wykonaj obliczenia zarówno za pomocą SSE, jak i równania, i upewnij się, że wyniki są wystarczająco bliskie, aby dać pewność. Napisz prostą wersję, nie dla szybkości, ale dla poprawności; następnie użyj jego wyników, aby sprawdzić, czy twoja sprytnie zoptymalizowana wersja dostała poprawną odpowiedź.

Legendarny John Carmack zaleca to drugie podejście; nazywa to "implementacjami równoległymi". Przeczytaj o tym artykuł: his essay.

Polecam najpierw napisać wersję uproszczoną. Następnie wróć i zacznij ponownie pisać części aplikacji za pomocą przyspieszenia SSE lub AVX i upewnij się, że przyspieszone wersje dają prawidłowe odpowiedzi. (Czasami wersja prosta może mieć błąd, którego nie ma wersja przyspieszona, a dwie wersje i porównanie pomagają wykryć błędy w obu wersjach.)

+2

Jeśli myślisz o optymalizacji, wątpię, czy chcesz wykonywać takie kontrole w pętli ... –

+0

Tak, wolisz umieścić pętlę wewnątrz funkcji wywoływanych dla każdej gałęzi 'switch'. –

+1

A nawet lepiej, mają klasę interfejsu, która jest rozszerzona i zaimplementowana przy użyciu 3 optymalizacji ... przełącznika polimorficznego. –

6

Istnieje kilka rozwiązań.

Jedna jest oparta na C++, gdzie można utworzyć wiele klas - zazwyczaj implementuje się klasę interfejsu i wykorzystuje funkcję fabryczną, aby podać obiekt właściwej klasy.

np.

class Matrix 
{ 
    virtual void Multiply(Matrix &result, Matrix& a, Matrix &b) = 0; 
    ... 
}; 

class MatrixPlain : public Matrix 
{ 
    void Multiply(Matrix &result, Matrix& a, Matrix &b); 

}; 


void MatrixPlain::Multiply(...) 
{ 
    ... implementation goes here... 
} 

class MatrixSSE: public Matrix 
{ 
    void Multiply(Matrix &result, Matrix& a, Matrix &b); 
} 

void MatrixSSE::Multiply(...) 
{ 
    ... implementation goes here... 
} 

... same thing for AVX... 

Matrix* factory() 
{ 
    switch(type_of_math) 
    { 
     case PlainMath: 
      return new MatrixPlain; 

     case SSEMath: 
      return new MatrixSSE; 

     case AVXMath: 
      return new MatrixAVX; 

     default: 
      cerr << "Error, unknown type of math..." << endl; 
      return NULL; 
    } 
} 

Albo, jak zasugerowano powyżej, można użyć współdzielonych bibliotek, które mają wspólny interfejs i dynamicznie załadować bibliotekę, że ma rację.

Oczywiście, jeśli zaimplementujesz klasę bazową Matrix jako klasę "zwykłą", możesz dokonać stopniowego udoskonalenia, a implementacja tylko tych części, które faktycznie znajdziesz, będzie korzystna, i polegaj na klasie bazowej, aby zaimplementować funkcje, w których wydajność nie jest " t wysoce krwisty.

Edytuj: Mówisz o linii i myślę, że patrzysz na nieprawidłowy poziom funkcji, jeśli tak jest. Potrzebujesz dość dużych funkcji, które robią coś na całkiem sporo danych. W przeciwnym razie wszystkie twoje wysiłki zostaną poświęcone na przygotowanie danych we właściwym formacie, a następnie wykonanie kilku instrukcji obliczeniowych, a następnie włożenie danych z powrotem do pamięci.

Zastanowiłabym się również, w jaki sposób przechowujesz swoje dane. Czy przechowujesz zestawy tablic z X, Y, Z, W, czy przechowujesz wiele X, dużo Y, dużo Z i dużo W w osobnych tablicach [zakładając, że robimy obliczenia 3D]? W zależności od sposobu obliczania może się okazać, że wykonanie jednej lub drugiej strony da najlepsze korzyści.

Zrobiłem sporo SSE i 3DNow! optymalizacje kilka lat temu, a "sztuczka" często jest bardziej związana z przechowywaniem danych, dzięki czemu można łatwo pobrać "pakiet" odpowiedniego rodzaju danych za jednym razem. Jeśli dane są przechowywane w niewłaściwy sposób, tracisz dużo czasu na "przeskakiwanie danych" (przenoszenie danych z jednego sposobu przechowywania na inny).

+0

+1 dla stopniowego udoskonalania –

+0

Problem z tym podejściem polega na tym, że nie można skompilować i zoptymalizować różnych funkcji dla różnych architektur. Jeśli wszystko jest skompilowane z powiedzmy '-march = i7' nawet wersja C będzie działać tylko na i7, jeśli kompilujesz z' -march = i686' będzie działać na każdej maszynie zbudowanej w ciągu ostatnich 15 lat, ale pewne wewnętrzne (jak SSE/AVX) nie będą dostępne, a optymalizator użyje tylko podzestawu dostępnych instrukcji w wersji SSE/AVX. – hirschhornsalz

+0

Więc zbuduj kod w oddzielnych plikach źródłowych.Chociaż uważam, że jeśli naprawdę chcesz używać instrukcji SSE/AVX w naprawdę dobry sposób, będziesz musiał użyć wbudowanego asemblera. Kompilator zwykle nie wykonuje tak dobrej pracy w "byciu inteligentnym". –