11
votes

C ++ Manipulation Spécifique IPL - #Ifdef vs Initiance VS Tag Dispatch

J'ai quelques classes mettant en œuvre des calculs que j'ai Pour optimiser pour différentes implémentations de SIMD par exemple. Altivec et SSE. Je ne veux pas poluter le code avec #Ifdef ... #endif code> blocs Pour chaque méthode, je dois optimiser pour que j'ai essayé quelques autres approches, mais nous ne sommes pas très satisfaits de la façon dont il s'est retourné Pour des raisons, je vais essayer de clarifier. Donc je cherche des conseils sur la façon dont je pourrais améliorer ce que j'ai déjà fait.

1. Différents de mise en œuvre avec brut comprend strand> p>

J'ai le même fichier d'en-tête décrivant l'interface de la classe avec différents Fichiers de mise en œuvre «pseudo» pour la plaine C ++, Altivec et SSE uniquement pour le Méthodes pertinentes: P>

// Algo.h
class Algo : private AlgoImpl
{
 ... as before
}

// AlgoImpl.h
#ifndef ALGOIMPL_H_INCLUDED_
#define ALGOIMPL_H_INCLUDED_
class AlgoImpl
{
protected:
    AlgoImpl();
    ~AlgoImpl();

   void computeSomeImpl();
   void computeMoreImpl();
};
#endif

// Algo.cpp
...
void Algo::computeSome()
{
    computeSomeImpl();
}
void Algo::computeMore()
{
    computeMoreImpl();
}

// Algo_SSE.cpp
AlgoImpl::AlgoImpl()
{
}
AlgoImpl::~AlgoImpl()
{
}
void AlgoImpl::computeSomeImpl()
{
}
void AlgoImpl::computeMoreImpl()
{
}
  • La scission est assez simple et facile à faire li>
  • Il n'y a pas de "frais généraux" (je ne sais pas comment le dire mieux) d'objets de ma classe Par lequel je veux dire aucun héritage supplémentaire, aucun ajout de variables de membre, etc. Li>
  • beaucoup plus propre que #ifdef code> -ing sur place li> ul>

    contre: p>

    • J'ai trois fichiers supplémentaires pour la maintenance; Je pourrais mettre le scalaire implémentation dans le fichier algo.cpp et finissez avec seulement deux mais le L'inclusion se trouve et est tombée un peu plus sale li>
    • Ce ne sont pas des unités compilables en soi et doivent être exclus de la Structure du projet LI>
    • Si je n'ai pas encore la mise en œuvre optimisée spécifique pour dire SSE je devrais dupliquer du code du fichier de mise en œuvre de la plaine (scalaire) C ++ LI>
    • Je ne peux pas revenir à la mise en œuvre plaine C ++ si elle est nue; ? Est-ce même possible faire cela dans le scénario décrit? LI>
    • Je ne ressens pas de cohésion structurelle dans l'approche li> ul>

      2. Dossiers de mise en œuvre 2.Différents avec héritage privé strong> p> xxx pré>

      avantages: p>

      • La scission est assez simple et facile à faire li>
      • beaucoup plus propre que #ifdef code> -ing sur place li>
      • toujours il n'y a pas de "frais généraux" à ma classe - Ebco devrait frapper li>
      • la sémantique de la classe est beaucoup plus propre, ce qui se compare au moins à ce qui précède c'est héritage privé == est implémenté en termes de code> li>
      • Les différents fichiers sont compilables, peuvent être inclus dans le projet. et sélectionné via le système de construction li> ul>

        contre: p>

        • J'ai trois fichiers supplémentaires pour la maintenance li>
        • Si je n'ai pas encore la mise en œuvre optimisée spécifique pour dire SSE je devrais dupliquer du code du fichier de mise en œuvre de la plaine (scalaire) C ++ LI>
        • Je ne peux pas revenir à la mise en œuvre plaine C ++ si elle est nue li> ul>

          3.is essentiellement méthode 2 mais avec des fonctions virtuelles dans la classe Algoiimpl forte>. Cette me permettrait de surmonter la mise en œuvre du code de code C ++ uni si nécessaire en fournissant une mise en œuvre vide dans la classe de base et en remplacement dans le dérivé Bien que je devais désactiver ce comportement lorsque je mettant en œuvre l'optimisation version. De plus, les fonctions virtuelles apporteront des "frais généraux" aux objets de ma classe. P>

          4.A FORMULAIRE DE DÉPUNTING VIA EN ABABLE_IF STRORT> P>

          p>

          • La scission est assez simple et facile à faire li>
          • beaucoup plus propre que #Ifdef ing sur tout le lieu Li>
          • Il n'y a toujours pas de "frais généraux" à ma classe li>
          • éliminera le besoin de différents fichiers pour différentes implémentations li> ul>

            contre: p>

            • Les modèles seront un peu plus "cryptiques" et semblent apporter une inutile frais généraux (au moins pour certaines personnes dans certains contextes) li>
            • Si je n'ai pas encore la mise en œuvre optimisée spécifique pour dire SSE je devrais dupliquer un code de la mise en œuvre de la plaine (scalaire) C ++ LI>
            • Je ne peux pas revenir à la mise en œuvre plaine C ++ si nécessaire LI> ul>

              Ce que je ne pouvais pas trouver encore pour aucune des variantes est de savoir comment correctement et replie bien à la mise en œuvre plaine C ++. p>

              aussi je ne veux pas trop d'ingénierie et à cet égard la première variante semble le plus "baiser" comme même envisager les inconvénients. P> p>


1 commentaires

Kiss est un directeur trompeur. La simplicité est difficile à définir (spécialement logicielle), vous devez visionner à écrire un logiciel qui peut faire face à de nouvelles spécifications. Le principal ouvert devrait être votre point de départ.


8 Réponses :


2
votes

Si la fonction virtuelle surcharge est acceptable, l'option 3 plus quelques ifdefs semble un bon compromis OMI. Il existe deux variantes que vous pourriez envisager:. Une classe de base abstraite, et l'autre avec la plaine mise en œuvre de C comme la classe de base

Avoir la mise en œuvre de C comme la classe de base vous permet d'ajouter progressivement les versions de vecteur optimisé, retour en baisse sur les versions non-vectorisés comme vous s'il vous plaît, en utilisant une interface abstraite serait un peu plus propre à lire. p>

en outre, ayant des versions séparées C ++ et vectorisés de votre classe, vous laissez des tests unitaires facilement écrire que p >

  • Assurez-vous que le code vectorisé donne le bon résultat (facile à gâcher tout ça, et le vecteur flottant registres peuvent avoir différents niveaux de précision que FPU, entraînant des résultats différents) li>
  • Comparer les performances du C ++ vs le vectorisé. Il est souvent bon de faire que le code vectorisé est en fait vous faire du bien. Compilateurs peut générer très serré code C ++ qui fait parfois aussi bien ou mieux que le code vectorisé. Li> Ul>

    Voici une avec les implémentations c ++ simple que la classe de base. Ajout d'une interface abstraite serait tout simplement ajouter une classe de base commune à tous les trois d'entre eux: p>

    // Algo.h:
    
     class Algo_Impl    // Default Plain C++ implementation
    {
    public:
         virtual ComputeSome();
         virtual ComputeSomeMore();
         ...
    };
    
    // Algo_SSE.h:
    class Algo_Impl_SSE : public Algo_Impl   // SSE
    {
    public:
         virtual ComputeSome();
         virtual ComputeSomeMore();
         ...
    };
    
    // Algo_Altivec.h:
    class Algo_Impl_Altivec : public Algo_Impl    // Altivec implementation
    {
    public:
         virtual ComputeSome();
         virtual ComputeSomeMore();
         ...
    };
    
    // Client.cpp:
    Algo_Impl *myAlgo = 0;
    #ifdef SSE
        myAlgo = new Algo_Impl_SSE;
    #elseif defined(ALTIVEC)
        myAlgo = new Algo_Impl_Altivec;
    #else
        myAlgo = new Algo_Impl_Default;
    #endif
    ...
    


1 commentaires

+1 Très bon point sur les tests de l'unité. J'ai envisagé d'utiliser le type d'approche que vous proposez, mais cela ne convient pas bien à ma situation principalement parce que cela nécessite plus de changements que je souhaiterais avoir - devrait changer de classe de base pour permettre une dérivation de, ajouter des méthodes virtuelles, fournir des usines à Débarrassez-vous de la dernière étape #Ifdef et découlant de la plaine C ++ impliquerait trois nouveaux types qui seront exposés au monde extérieur (j'aimerais vraiment masquer complètement les implémentations).



1
votes

5 commentaires

Je ne pense pas que ma situation soit apte à appliquer le modèle de l'adaptateur. Si vous coupez le motif sur son minimum nu, il est utilisé pour adapter une interface à une autre qui n'est pas mon cas ici. Je n'ai pas déjà d'implémentations différentes qui doivent être utilisées de manière uniforme, je dois réellement fournir les nouvelles implémentations et j'ai choisi de satisfaire la même interface. Il serait vraiment non naturel d'avoir cela juste pour soutenir le modèle. Je pense que ma situation convient davantage avec le motif de décorateur, mais j'ai choisi de ne pas aller de cette façon parce que je pense que cela apportera une surcharge inutile


Et après tout, j'essaie d'optimiser les choses là-bas. Je ne pense pas que l'approche que vous suggérez qu'il soit viable pour mon cas particulier, en passant par voie générique d'abord, puis détaillez à ma langue et à ma mise en œuvre particulières, car j'ai déjà la mise en œuvre scalaire en place, qui fait partie intégrante d'une librairie avec une structure établie et une hiérarchie de classe, et ce que je dois faire est de fournir les implémentations "optimisées" en essayant de les adapter sans repenser complètement la chose et ajouter trop de frais généraux.


Je n'essaie pas d'accéder à une discussion sur les modèles de conception ici ou de discuter si vous vous concentrez sur les détails de la mise en œuvre au début, c'est la bonne façon d'aller, bien que je puisse constater sincèrement toutes les directives d'intrants, d'idées ou de conception données. Je suis vraiment intéressé ici sur la méthode "meilleure" pour améliorer ma solution particulière dans le contexte de ma langue particulière.


Donc, il n'y a pas d'entités existant pour que vous souhaitiez vous adapter. Vous allez implémenter les algorithmes et vous souhaitez autoriser la sélection de ces algorithmes. Je le vois comme candidat de la stratégie, mais je ne me fais pas de penser à la pensée. Je ne parle pas d'approche classique. Le modèle de stratégie ne peut avoir de frais de main-d'œuvre -> MetaProgramming. Et vous pouvez facilement revenir à la stratégie concrète par défaut. Voir Hillside.net/plop/2006/papers/library/portableProgrammingPL. PDF


Papier intéressant - Les variantes de stratégie présentées pourraient être de bons candidats pour essayer d'améliorer mon implours.



7
votes

Vous pouvez utiliser une approche basée sur la stratégie avec des modèles tels que la bibliothèque standard pour les allocateurs, les comparateurs et similaires. Chaque implémentation a une classe politique qui définit ComputeomeomeomeomeomeomeMore (). Votre classe d'algo prend une stratégie comme un paramètre et des illustrations à sa mise en œuvre.

template <class algo_t>
void use_algo(algo_t algo)
{
    algo.process();
}

void select_algo(bool use_scalar)
{
    if (!use_scalar) {
        use_algo(algo_default_t());
    } else {
        use_algo(algo_scalar_t());
    }
}


5 commentaires

Idée intéressante. Et je pense que je pourrais simplement paramétrer uniquement les méthodes et ce sera quelque chose de similaire au modèle de stratégie suggéré par @mloskot dans ses commentaires. Une observation cependant - je ne pourrai pas compiler toutes les variantes dans le même programme que Altivec et SSE sont exclusives mutuelles (appartiennent à différentes architectures matérielles).


+1, c'est essentiellement ce que j'ai fait dans un scénario similaire, bien que j'ai eu un stratégie_list , donnant la possibilité de disposer de plusieurs stratégies actives dans une version donnée et une "chute" pour permettre l'expression de la préférence. Pour un certain nombre d'implémentations (Altivec, SSE Variantes, OPENCL et une option pure C ++)


J'ai utilisé une approche basée sur des stratégies pour gérer le code qui devait être 32 bits ou 64 bits.


@awoodland pourriez-vous donner un petit exemple avec la liste de stratégies et de l'approche "Fall à" que vous employiez?


@celavek - J'ai ajouté un exemple rapide de ce que j'ai fait. Le code réel était un peu moins propre car il a fallu des expressions plus généralisées comme un autre paramètre de modèle sur appliquer () , mais le même principe s'applique.



0
votes

Afin de masquer les détails de la mise en œuvre, vous pouvez simplement utiliser une interface abstraite avec le créateur statique et fournir trois classes de mise en œuvre:

AlgoPtr algo = Algo::Create("SSE");
algo->Process();


0 commentaires

0
votes

Il me semble que votre première stratégie, avec des fichiers C ++ séparés et une notamment la mise en œuvre spécifique, est la plus simple et la plus propre. J'ajouterais quelques commentaires à votre algo.cpp indiquant quelles méthodes sont dans les fichiers #inclus.
E.G.

// Algo.cpp
#include "Algo.h"
Algo::Algo() { }

Algo::~Algo() { }

void Algo::process()
{
    computeSome();
    computeMore();
}

// The following methods are implemented in separate, 
// platform-specific files.
// void Algo::computeSome()
// void Algo::computeMore()

#if defined(ALTIVEC)
    #include "Algo_Altivec.cpp" 
#elif defined(SSE)
    #include "Algo_SSE.cpp"
#else
    #include "Algo_Scalar.cpp"
#endif


2 commentaires

Comment est-ce différent de ma première méthode? :) Si vous regardez la liste, vous trouverez un commentaire avant les implémentations de la méthode "// algo_altivec.cpp", puis à la fin "... même pour les autres fichiers de mise en œuvre".


Il n'est pas destiné à être très différent de votre première méthode. J'ai dit: "Il me semble que votre première stratégie, avec des fichiers C ++ séparés et #incluant la mise en œuvre spécifique, est le plus simple et le plus propre.". Je viens de suggérer, en outre, de mettre des commentaires avant la #Inclus d'indiquer quelles méthodes figuraient dans le fichier incluant. Cela signifie que quelqu'un lisant le fichier principal .CPP n'a pas besoin d'ouvrir les fichiers inclus pour voir quel est leur contenu.



1
votes

Ce n'est pas vraiment une réponse entière: une variante sur l'une de vos options existantes. Dans l'option 1 Vous avez supposé que vous incluez Algo_altivec.cpp & c. En Algo.cpp, mais vous n'avez pas à faire cela. Vous pouvez également omettre Algo.cpp entièrement et avoir votre système de construction décider lequel d'algo_altivec.cpp, algo_sse.cpp, & c. construire. Vous devriez faire quelque chose comme celui-ci de toute façon, quelle que soit l'option que vous utilisez, car chaque plate-forme ne peut pas compiler chaque mise en œuvre; Ma suggestion est que celle de l'option que vous choisissez, au lieu d'avoir #IF Altivec_Enabled partout dans la source, où Altivec_Enableabled est défini à partir du système de construction, vous avez simplement le système de construction décider directement s'il faut compiler algo_altivec.cpp. Ceci est un peu plus délicat à réaliser dans MSVC que de faire, SCONS, & C., mais toujours possible. Il est courant de basculer dans un répertoire entier plutôt que des fichiers source individuels; C'est au lieu d'algo_altivec.cpp et d'amis, vous auriez plate-forme / altivec / algo.cpp, plate-forme / sse / algo.cpp, etc. De cette façon, lorsque vous avez un deuxième algorithme, vous avez besoin d'implémentations spécifiques à la plate-forme, vous pouvez simplement ajouter le fichier source supplémentaire à chaque répertoire.

Bien que ma suggestion soit principalement destinée à être une variante de l'option 1, vous pouvez le combiner avec n'importe laquelle de vos options, vous permettant de décider du système de construction et au moment de l'exécution des options d'exécution. Dans ce cas, vous aurez probablement besoin de fichiers d'en-tête spécifiques à la mise en œuvre aussi.


0 commentaires

0
votes

Les modèles de type de stratégie (mélanges) sont corrects jusqu'à ce que l'exigence retourne à la mise en œuvre par défaut . C'est l'opoption d'exécution et devrait être géré par le polymorphisme d'exécution. modèle de stratégie peut gérer cette amende.

Il y a un inconvénient de cette approche: Stratégie ALGORITHIQUE QUELLE Mise en œuvre ne peut être induite. Une telle influence peut fournir une amélioration raisonnable des performances dans de rares cas. S'il s'agit d'un problème, vous devez couvrir la logique de niveau supérieur par stratégie .


0 commentaires

2
votes

Comme demandé dans les commentaires, voici un résumé de ce que j'ai fait:

Configuration Politicle_list Utilitaire de modèle d'assistance

Ceci maintient une liste de stratégies et leur donne Un appel "Vérification de l'exécution" Avant d'appeler les premières stratégies spécifiques à la configuration de la première implémentation

xxx

Ces stratégies mettent à la fois un test d'exécution et une mise en œuvre réelle de l'algorithme en question. Pour mon Problème réel, implémente un autre paramètre de modèle spécifié ce que c'était exactement implémentant, ici même si l'exemple suppose qu'il n'y a qu'une seule chose à mettre en œuvre. Les tests d'exécution sont mis en cache dans un statique bool pour certains (par exemple, l'ALTIVEC IT utilisé) Le test était vraiment lent. Pour d'autres (par exemple, l'opencl One), le test est en fait "est ce pointeur de fonction null ?" Après une tentative de la définition avec dlsym () . xxx

SET par architecture stratégie_list Exemple trivial Définit l'une des deux listes possibles basées sur arch_has_sse PreProcessor Macro. Vous pouvez générer ceci à partir de votre script de construction ou utiliser une série de Typedef S ou pirater la prise en charge de "trous" dans "code> stratégie_list qui pourrait être annulé sur certaines architectures sautant droites au suivant, sans essayer de vérifier le soutien. GCC définit certains macors de préprocesseur pour vous aider, par ex. __ sse2 __ . xxx

Vous pouvez également l'utiliser pour compiler plusieurs variantes sur la même plate-forme, par exemple. et SSE et non-SSE binaire sur x86.

Utilisez la liste des stratégies

assez simple, appelez le Apply () Méthode statique sur le politique_list . Confiance qu'il appellera la méthode IPL () sur la première stratégie qui transmet le test d'exécution. xxx

si vous prenez le "par modèle d'opération "Approche que j'ai mentionnée plus tôt, cela pourrait être quelque chose de plus comme: xxx

dans ce cas, vous finissez par faire de votre matrice et vecteur Types Conscient du Politique_List Pour qu'ils puissent décider comment / où stocker les données. Vous pouvez également utiliser des heuristiques pour cela aussi, par ex. "Petit vectoriel / matrice vit dans la mémoire principale, quoi que ce soit" et faire le runtime_check () ou une autre fonction tester la pertinence d'une approche particulière d'une implémentation donnée pour une instance spécifique. < P> J'ai également eu un allocator personnalisé pour les conteneurs, qui produisait une mémoire alignée appropriée toujours sur une construction activée par SSE / Altivec, que la machine spécifique avait la prise en charge de l'Altivec. Il était tout simplement plus facile de cette façon, bien qu'il puisse s'agir d'un Typedef dans une stratégie donnée et que vous supposez toujours que la politique de priorité la plus élevée a les besoins les plus stricts d'allocator.

Exemple have_altivec () :

J'ai inclus un exemple d'exemple avoir_altivec () implémentation de l'exhaustivité, simplement parce que c'est le plus court et donc le plus approprié pour la publication ici. Le CPUID X86 / X86_64 est désordonné car vous devez prendre en charge les modes d'écriture spécifiques du compilateur d'ASM en ligne. L'OpenCl One est désordonné parce que nous vérifions également certaines des limites et des extensions de mise en œuvre. xxx

conclusion

Fondamentalement, vous ne payez aucune pénalité pour les plateformes qui ne peuvent jamais Soutien d'une implémentation (le compilateur ne génère aucun code pour eux) et une petite pénalité (potentiellement un très prévisible par la paire CPU Test / JMP si votre compilateur est à moitié décent à l'optimisation) pour les plates-formes pouvant supporter quelque chose mais pas . Vous ne payez aucun coût supplémentaire pour les plates-formes que la mise en œuvre du premier choix est exécutée. Les détails des tests d'exécution varient en fonction de la technologie en question.


0 commentaires