Напишите одно универсальное ядро и сопоставьте его с разными ISA

#c #code-generation #template-meta-programming #generic-programming

#c #генерация кода #шаблон-мета-программирование #generic-программирование

Вопрос:

ОБНОВЛЕНИЕ II: я написал два примера, чтобы проиллюстрировать идеи, предложенные в принятом ответе и комментариях. Первый math_cmp.cc выполняет явно типизированные операции.

 // 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 *) amp;res_vec;
    for (int i = 0; i < 4;   i)
    cout << "RES[" << i << "]: " << res_ptr[i] << ' ';
    cout << endl;

    return 0;
}
  

Второй файл math_traits.cc выполняет шаблон черты. Сборка, созданная при компиляции с -O3 помощью, почти идентична 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 *) amp;res_vec;
    for (int i = 0; i < 4;   i)
    cout << "RES[" << i << "]: " << res_ptr[i] << ' ';
    cout << endl;

    return 0;
}
  

ОБНОВЛЕНИЕ I: Я думаю, вопрос можно резюмировать следующим образом: «Существует ли современный подход C , который эквивалентен полиморфным функциям, подобным макросам?»


Интересно, можно ли запрограммировать на C для написания одного ядра с абстрактными операциями и автоматического создания специфичных для ISA кодов. Например, универсальное ядро может быть:

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

И ядро может быть преобразовано в обе скалярные версии:

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

и векторизованная версия:

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

На самом деле универсальные ядра были бы намного сложнее. Универсальность типов может быть обработана параметрами шаблона. Но универсальность инструкций заставила меня застрять.

Обычно такого рода проблемы решаются с помощью генерации кода, когда вы пишете программу в некотором промежуточном представлении (IR), а затем переводите выражение IR на другой целевой язык.

Однако я должен делать это в чистом и современном C , что означает отсутствие макросов C. Интересно, достижимо ли это, умело используя универсальное программирование, шаблонное метапрограммирование или ООП. Пожалуйста, помогите, если у вас есть какие-то указания!

Комментарии:

1. Исследовали ли вы существующие библиотеки для параллельного программирования на основе шаблонов в C ? Однако в вашем последнем абзаце создается впечатление, что это домашнее задание.

2. @JAB Я бы предположил, что вопрос выходит за рамки обычного домашнего задания на C . Но я могу ошибаться. Ограничение в последнем абзаце связано с существующим дизайном проекта.

3. @JAB. Добавлены примеры, иллюстрирующие идею. Дайте мне знать, если это имеет смысл. Спасибо.

Ответ №1:

Как правило, это достижимо с помощью шаблонов и признаков типа:

 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.
  

Однако этот подход может привести к сбою, если вы захотите по-разному обрабатывать один и тот же тип в разных ситуациях. Но это общий предел перегрузки…

В приведенном примере на самом деле возможно напрямую специализировать функцию, но описанный способ является более распространенным, поскольку он более понятен и используется повторно.

Комментарии:

1. Спасибо @Zbynek Vyskovsky — kvr000. Похоже, это может сработать. Но я предполагаю, что производительность ухудшится из-за использования класса-оболочки MathTraits и накладных расходов на вызов функции. Я мог бы принудительно встроить специализированные версии add , но я сомневаюсь, что это сильно помогло бы.

2. Я думаю, вопрос можно резюмировать следующим образом: «Существует ли современный подход C , который эквивалентен полиморфным функциям, подобным макросам?»

3. @K.G. «Существует ли современный подход C , который эквивалентен макросам, подобным полиморфным функциям?» Да, использование шаблонов и признаков типа (хотя истинный полиморфизм может быть сложно реализовать с помощью аргументов шаблона в зависимости от того, что вы пытаетесь сделать). Хотя производительность, безусловно, ухудшится из-за использования класса-оболочки / накладных расходов на вызовы функций в неоптимизированном коде, большинство современных компиляторов автоматически вставляют такие короткие фрагменты при использовании оптимизированных сборок, так что все должно быть в порядке.