2

MISE À JOUR II: J'ai écrit deux exemples pour illustrer les idées proposées dans la réponse acceptée et les commentaires. Le premier math_cmp.cc effectue des opérations typées explicites.Écrire un noyau générique et le mapper à différents ISA

// math_cmp.cc 
#include <iostream> 
#include <xmmintrin.h> 

using namespace std; 

int main() 
{ 
    float a, b; 
    cin >> a >> b; 

    float res = (a + b) * (a - b); 

    cout << res << endl; 

    __m128 a_vec, b_vec, res_vec; 
    a_vec = _mm_set1_ps(a); 
    b_vec = _mm_set1_ps(b); 
    res_vec = _mm_mul_ps(_mm_add_ps(a_vec, b_vec), 
      _mm_sub_ps(a_vec, b_vec)); 

    float *res_ptr; 
    res_ptr = (float *) &res_vec; 
    for (int i = 0; i < 4; ++i) 
    cout << "RES[" << i << "]: " << res_ptr[i] << ' '; 
    cout << endl; 

    return 0; 
} 

Le second fichier math_traits.cc exécute les traits modèle +. L'assembly généré lorsqu'il est compilé avec -O3 est presque identique à celui de math_cmp.cc.

// math_traits.cc 
#include <iostream> 
#include <xmmintrin.h> 

using namespace std; 

template <typename T> 
class MathOps 
{ 
}; 

template <typename T> 
T kernel (T a, T b) 
{ 
    T res = MathOps<T>::mul(MathOps<T>::add(a, b), MathOps<T>::sub(a, b)); 

    return res; 
} 

template <> 
class MathOps <float> 
{ 
public: 
    static float add (float a, float b) 
    { 
    return a + b; 
    } 

    static float sub (float a, float b) 
    { 
    return a - b; 
    } 

    static float mul (float a, float b) 
    { 
    return a * b; 
    } 
}; 

template <> 
class MathOps <__m128> 
{ 
public: 
    static __m128 add (__m128 a, __m128 b) 
    { 
    return a + b; 
    } 

    static __m128 sub (__m128 a, __m128 b) 
    { 
    return a - b; 
    } 

    static __m128 mul (__m128 a, __m128 b) 
    { 
    return a * b; 
    } 
}; 

int main() 
{ 
    float a, b; 
    cin >> a >> b; 
    cout << kernel<float>(a, b) << endl; 

    __m128 a_vec, b_vec, res_vec; 
    a_vec = _mm_set1_ps(a); 
    b_vec = _mm_set1_ps(b); 
    res_vec = kernel<__m128>(a_vec, b_vec); 

    float *res_ptr; 
    res_ptr = (float *) &res_vec; 
    for (int i = 0; i < 4; ++i) 
    cout << "RES[" << i << "]: " << res_ptr[i] << ' '; 
    cout << endl; 

    return 0; 
} 

MISE À JOUR I: Je suppose que la question peut se résumer comme: "? Y at-il une approche C++ moderne qui équivaut à polymorphes fonction comme macros"


Je me demande s'il est possible de programmer en C++ pour écrire un noyau avec les opérations abstraites et produire automatiquement des codes spécifiques ISA. Par exemple, un noyau générique peut être:

RET_TYPE kernel(IN_TYPE a, IN_TYPE b) 
{ 
    RET_TYPE res = ADD(a, b); 
    return res; 
} 

Et le noyau peut être transformé en à la fois une version scalaire:

float kernel(float a, float b) 
{ 
    float res = a + b; 
    return res; 
} 

et une version vectorisée:

__m128 kernel(__m128 a, __m128 b) 
{ 
    __m128 res = _mm_add_ps(a, b); 
    return res; 
} 

En réalité, les noyaux génériques seraient beaucoup plus complexes. La généricité dans les types peut être gérée par des paramètres de gabarit. Mais la généricité des instructions m'a bloqué.

Habituellement, ce genre de problème est résolu par de génération de code, où vous écrivez le programme dans une représentation intermédiaire (IR) puis traduire l'expression IR dans différentes langue cible.

Cependant, je dois le faire dans pur et moderne C++, ce qui signifie pas de macros C. Je me demande si c'est réalisable en exploitant intelligemment Programmation générique, Modèle Metaprogramming ou OOP. S'il vous plaît aider si vous avez des pointeurs!

+0

Avez-vous étudié les bibliothèques existantes pour la programmation parallèle basée sur un modèle en C++? Votre dernier paragraphe donne l'impression que c'est une tâche à faire, cependant. – JAB

+0

@JAB Je suppose que la question est au-delà de la portée habituelle de toute tâche de devoir C++. Mais je peux me tromper.La contrainte dans le dernier paragraphe est due à la conception du projet existant. –

+0

@JAB. Ajout des exemples pour illustrer l'idée. Faites-moi savoir si cela a du sens. Je vous remercie. –

Répondre

2

En général, il est possible avec des modèles et des traits de type:

template <typename T> 
T kernel(T a, T b) 
{ 
    return MathTraits<T>::add (a, b); 
} 
template <typename T> 
class MathTraits 
{ 
} 
// specialization for float 
template <> 
class MathTraits <float> 
{ 
    float add(float a, float b) 
    { 
      return a+b; 
    } 
} 
// specialization of MathTraits for __m128 etc. 

Cependant, cette approche peut échouer lorsque vous voudrez peut-être traiter différemment le même type dans des situations différentes. Mais c'est la limite générale de la surcharge ...

Avec l'exemple donné, il est réellement possible de spécialiser directement la fonction, mais la manière décrite est plus courante car elle est plus claire et réutilisable.

+0

Merci @Zbynek Vyskovsky - kvr000. Cela semble fonctionner. Mais je suppose que les performances se détérioreraient en raison de l'utilisation de la classe wrapper 'MathTraits' et de la surcharge de l'appel de fonction. Je pourrais forcer les versions spécialisées de 'add', mais je doute que ça aide beaucoup. –

+0

Je suppose que la question peut être résumée comme suit: ** "Existe-t-il une approche C++ moderne qui équivaut à des macros de type fonction polymorphique?" ** –

+1

@ K.G. "Y a-t-il une approche C++ moderne qui soit équivalente aux macros polymorphes fonctionnelles?" Oui, utilisation de modèles et de caractères de type (bien que le vrai polymorphisme puisse être difficile à extraire avec des arguments de modèle en fonction de ce que vous essayez de faire). Alors que les performances vont certainement se dégrader en raison de l'utilisation de la classe wrapper dans le code non optimisé, la plupart des compilateurs modernes aligneront automatiquement de tels fragments lors de l'utilisation de builds optimisés. – JAB