Avoir différentes optimisations (brut, SSE, AVX) dans le même exécutable avec C / C ++

Je développe des optimisations pour mes calculs 3D et j’ai maintenant:

  • une version ” plain ” utilisant les bibliothèques standard du langage C,
  • une version optimisée pour SSE #define USE_SSE aide d’un préprocesseur #define USE_SSE ,
  • une version optimisée AVX #define USE_AVX aide d’un préprocesseur #define USE_AVX

Est-il possible de basculer entre les 3 versions sans avoir à comstackr différents exécutables (par exemple, avoir des fichiers de bibliothèque différents et charger le “bon” dynamicment, ne sais pas si inline fonctions inline sont “correctes” pour cela)? Je considérerais aussi les performances d’avoir ce genre de commutateur dans le logiciel.

Il y a plusieurs solutions pour cela.

L’une est basée sur C ++, dans laquelle vous créez plusieurs classes. En règle générale, vous implémentez une classe d’interface et utilisez une fonction fabrique pour vous donner un object de la classe correcte.

par exemple

 class Masortingx { virtual void Multiply(Masortingx &result, Masortingx& a, Masortingx &b) = 0; ... }; class MasortingxPlain : public Masortingx { void Multiply(Masortingx &result, Masortingx& a, Masortingx &b); }; void MasortingxPlain::Multiply(...) { ... implementation goes here... } class MasortingxSSE: public Masortingx { void Multiply(Masortingx &result, Masortingx& a, Masortingx &b); } void MasortingxSSE::Multiply(...) { ... implementation goes here... } ... same thing for AVX... Masortingx* factory() { switch(type_of_math) { case PlainMath: return new MasortingxPlain; case SSEMath: return new MasortingxSSE; case AVXMath: return new MasortingxAVX; default: cerr << "Error, unknown type of math..." << endl; return NULL; } } 

Ou, comme suggéré ci-dessus, vous pouvez utiliser des bibliothèques partagées ayant une interface commune et charger dynamicment la bibliothèque appropriée.

Bien entendu, si vous implémentez la classe de base Masortingx en tant que classe "standard", vous pouvez procéder à un raffinement pas à pas. Seules les parties que vous trouvez réellement utiles peuvent être mises à profit. Vous pouvez également compter sur la classe de base pour implémenter les fonctions où les performances ne sont pas très critiques.

Edit: Vous parlez de inline, et je pense que vous regardez le mauvais niveau de fonction si c'est le cas. Vous voulez des fonctions assez volumineuses qui font quelque chose avec beaucoup de données. Sinon, tous vos efforts seront consacrés à la préparation des données dans le bon format, à quelques instructions de calcul, puis à la remise en mémoire des données.

Je voudrais aussi examiner comment vous stockez vos données. Stockez-vous des ensembles d'un tableau avec X, Y, Z, W ou stockez-vous beaucoup de X, beaucoup de Y, beaucoup de Z et beaucoup de W dans des tableaux séparés [en supposant que nous effectuons des calculs 3D]? Selon le fonctionnement de votre calcul, vous constaterez peut-être que vous obtiendrez le meilleur bénéfice en procédant de l'une ou l'autre manière.

J'ai fait pas mal de SSE et de 3DNow! Il y a quelques années, l'optimisation a été optimisée et le «truc» concerne souvent la manière dont vous stockez les données, de sorte que vous pouvez facilement saisir un «paquet» du bon type de données en une fois. Si vous avez mal stocké les données, vous perdrez beaucoup de temps à "balayer des données" (déplacer des données d’un mode de stockage à un autre).

Une solution consiste à implémenter trois bibliothèques conformes à la même interface. Avec les bibliothèques dynamics, vous pouvez simplement échanger le fichier de bibliothèque et l’exécutable utilisera tout ce qu’il trouvera. Par exemple, sous Windows, vous pouvez comstackr trois DLL:

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

Et ensuite, établissez le lien exécutable contre Impl.dll . Maintenant, placez l’une des trois DLL spécifiques dans le même répertoire que le .exe , renommez-le en Impl.dll et utilisez cette version. Le même principe devrait essentiellement être applicable sur un système d’exploitation de type UNIX.

La prochaine étape consisterait à charger les bibliothèques par programme, ce qui est probablement la plus flexible, mais cela dépend du système d’exploitation et nécessite du travail supplémentaire (comme ouvrir la bibliothèque, obtenir des pointeurs de fonction, etc.).

Edit: Mais bien sûr, vous pouvez simplement implémenter la fonction trois fois et en sélectionner une au moment de l’exécution, en fonction du paramétrage / fichier de configuration etc., comme indiqué dans les autres réponses.

Bien sûr que c’est possible.

La meilleure façon de le faire est d’avoir des fonctions qui font le travail complet et de les sélectionner au moment de l’exécution. Cela fonctionnerait mais n’est pas optimal:

 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 } } } 

À chaque passage dans la boucle, le code exécute une instruction switch , ce qui est simplement une surcharge. Un compilateur très intelligent pourrait théoriquement résoudre ce problème pour vous, mais mieux le faire vous-même.

A la place, écrivez trois fonctions distinctes, une pour les utilisateurs ordinaires, une pour SSE et une pour AVX. Puis, au moment de l'exécution, décidez lequel exécuter.

Pour les points bonus, dans une construction "de débogage", effectuez le calcul à la fois avec le SSE et avec la plaine et affirmez que les résultats sont suffisamment proches pour donner confiance. Écrivez la version simple, non pour la rapidité, mais pour la correction; utilisez ensuite ses résultats pour vérifier que vos versions optimisées et intelligentes obtiennent la bonne réponse.

Le légendaire John Carmack recommande cette dernière approche; il l'appelle "implémentations parallèles". Lire son essai à ce sujet.

Je vous recommande donc de commencer par écrire la version standard. Revenez ensuite en arrière et commencez à réécrire des parties de votre application en utilisant l'accélération SSE ou AVX et assurez-vous que les versions accélérées donnent les bonnes réponses. (Et parfois, la version ordinaire peut comporter un bogue, contrairement à la version accélérée. Avoir deux versions et les comparer permet de faire apparaître des bogues dans l'une ou l'autre version.)