C++ обработка конкретного impl - #ifdef vs private inheritance vs tag dispatch

у меня есть несколько классов, реализующих некоторые вычисления, которые у меня есть оптимизировать для различных реализаций SIMD, например Altivec и ЕГЭ. Я не хочу загрязнять код с помощью #ifdef ... #endif блоки для каждого метода я должен оптимизировать, поэтому я попробовал несколько других подходит, но unfotunately я не очень доволен тем, как это получилось по причинам, которые я постараюсь прояснить. Так что я ищу совета. о том, как я мог бы улучшить то, что уже сделано.

1.Отличающийся файлы реализации с crude включают

у меня есть тот же файл заголовка, описывающий интерфейс класса с разными "псевдо" файлы реализации для простого C++, Altivec и SSE только для соответствующие методы:

// Algo.h
#ifndef ALGO_H_INCLUDED_
#define ALGO_H_INCLUDED_
class Algo
{
public:
    Algo();
    ~Algo();

    void process();
protected:
    void computeSome();
    void computeMore();
};
#endif

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

Algo::~Algo() { }

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

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

// Algo_Altivec.cpp
void Algo::computeSome()
{
}
void Algo::computeMore()
{
}
... same for the other implementation files

плюсы:

  • разделение довольно просто и легко сделать
  • нет"накладных расходов" (не знаю, как сказать это лучше) для объектов моего класса под этим я подразумеваю отсутствие дополнительного наследства, переменные-члены и т. д.
  • гораздо чище, чем #ifdef - ing повсюду

плюсы:

  • у меня есть три дополнительных файла для обслуживания; я мог бы поставить скаляр внедрение в алгоритм.cpp файл, хотя и в конечном итоге с двумя, но включение часть будет выглядеть и упал немного грязнее
  • они не являются компилируемыми единицами per-se и должны быть исключены из структура проекта
  • если у меня нет конкретные оптимизированная реализация еще скажем SSE мне пришлось бы дублировать некоторый код из простого (скалярного) файла реализации c++
  • я не могу вернуться к простой реализации C++, если nedded; ? это вообще возможно сделать это в описанном сценарии ?
  • я не чувствую никакой структурной сплоченности в подходе

2.Различные файлы реализации с частным наследство

// 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()
{
}

плюсы:

  • разделение довольно просто и легко сделать
  • гораздо чище, чем #ifdef - ing повсюду
  • до сих пор нет" накладных расходов " на мой класс - EBCO должен пнуть
  • семантика класса намного чище, по крайней мере, по сравнению с приведенным выше это private inheritance == is implemented in terms of
  • различные файлы компилируются, могут быть включены в проект и выбран через систему сборки

плюсы:

  • у меня есть три дополнительных файла для обслуживания
  • если у меня еще нет конкретной оптимизированной реализации, скажем, SSE мне пришлось бы дублировать некоторый код из простого (скалярного) файла реализации c++
  • я не могу вернуться к простой реализации C++, если nedded

3.В основном Метод 2, но с виртуальными функциями в AlgoImpl класс. Что позволит мне преодолеть дублирующую реализацию простого кода C++, если это необходимо предоставляя пустой реализации в базовом классе и переопределить в производном хотя мне придется отключить это поведение, когда я фактически реализую оптимизированный версия. Также виртуальные функции принесут некоторые "накладные расходы" для объектов моего класса.

4.Форма отправки тегов через enable_if

плюсы:

  • в split довольно просто и легко сделать
  • гораздо чище, чем #ifdef ing во всем месте
  • все еще нет "накладных расходов" на мой класс
  • устранит необходимость в разных файлах для разных реализаций

плюсы:

  • шаблоны будут немного более "загадочными" и, похоже, принесут ненужное накладные расходы(по крайней мере для некоторых людей в некоторых ситуациях)
  • если у меня нет конкретных оптимизированная реализация еще, скажем так SSE мне пришлось бы дублировать некоторый код из простой (скалярной) реализации c++
  • я не могу по отношению к простой реализации C++, если это необходимо

что я не мог понять, но для любого из вариантов как правильно и чисто резервный вариант простой реализации C++.

также я не хочу, чтобы осложнить вещи, и в этом отношении первый вариант кажется, самый "поцелуй" нравится даже учитывая недостатки.

8 ответов


как указано в комментариях, Вот краткое изложение того, что я сделал:

настройка policy_list утилита шаблона помощника

это поддерживает список политик и дает им вызов "проверка выполнения" перед вызовом первой подходящей реализации

#include <cassert>

template <typename P, typename N=void>
struct policy_list {
  static void apply() {
    if (P::runtime_check()) {
      P::impl();
    }
    else {
      N::apply();
    }
  }
};

template <typename P>
struct policy_list<P,void> {
  static void apply() {
    assert(P::runtime_check());
    P::impl();
  }
};

настройка конкретных политик

эти политики реализуют как тест выполнения, так и фактическую реализацию рассматриваемого алгоритма. Для моего фактический проблема impl взял другой параметр шаблона, который указал, что именно они реализовывали, здесь, хотя пример предполагает, что есть только одна вещь, которая должна быть реализована. Тесты среды выполнения кэшируются в static bool для некоторых (например, Altivec, который я использовал) тест был очень медленным. Для других (например, OpenCL) тест на самом деле "является ли эта функция указателем NULL?- после одной попытки установить его с помощью dlsym().

#include <iostream>

// runtime SSE detection (That's another question!)
extern bool have_sse();

struct sse_policy {
  static void impl() {
    std::cout << "SSE" << std::endl;
  }

  static bool runtime_check() {
    static bool result = have_sse();
    // have_sse lives in another TU and does some cpuid asm stuff
    return result;
  }
};

// Runtime OpenCL detection
extern bool have_opencl();

struct opencl_policy {
  static void impl() {
    std::cout << "OpenCL" << std::endl;
  }

  static bool runtime_check() {
    static bool result = have_opencl();
    // have_opencl lives in another TU and does some LoadLibrary or dlopen()
    return result;
  }
};

struct basic_policy {
  static void impl() {
    std::cout << "Standard C++ policy" << std::endl;
  }

  static bool runtime_check() { return true; } // All implementations do this
};

установить на архитектуру policy_list

тривиально пример устанавливает один из двух возможных списков на основе ARCH_HAS_SSE макрос препроцессора. Вы можете создать это из своего сценария сборки или использовать серию typedefs, или взломать поддержку "отверстия" в policy_list это может быть пустым на некоторых архитектурах, пропуская прямо к следующему, не пытаясь проверить поддержку. GCC устанавливает некоторые препроцессорные macors для вас, которые могут помочь, например __SSE2__.

#ifdef ARCH_HAS_SSE
typedef policy_list<opencl_policy,
        policy_list<sse_policy,
        policy_list<basic_policy
                    > > > active_policy;
#else
typedef policy_list<opencl_policy,
        policy_list<basic_policy
                    > > active_policy;
#endif

вы можете использовать это для компиляции нескольких вариантов на одной платформе, например, и Двоичный файл SSE и no-SSE на x86.

список политика

довольно просто, позвоните apply() статический метод на policy_list. Поверьте, что он вызовет impl() метод в первой политике, которая проходит тест выполнения.

int main() {
  active_policy::apply();
}

если вы возьмете подход "за шаблон операции", о котором я упоминал ранее, это может быть что-то вроде:

int main() {
  Matrix m1, m2;
  Vector v1;

  active_policy::apply<matrix_mult_t>(m1, m2);
  active_policy::apply<vector_mult_t>(m1, v1);
}

в этом случае вы в конечном итоге сделать свой Matrix и Vector типы осознает policy_list для того, чтобы они могли решить, как / где хранить данные. Вы также можете использовать эвристику для этого, например, "малый вектор / матрица живет в основной памяти независимо от того, что" и сделать runtime_check() или другая функция проверяет уместность конкретного подхода к данной реализации для конкретного экземпляра.

у меня также был пользовательский распределитель для контейнеров, который создавал соответствующим образом выровненную память всегда на любой сборке с поддержкой SSE/Altivec, независимо от того, машина имела поддержку Altivec. Так было проще, хотя это могло быть и--12--> в данной политике, и вы всегда предполагаете, что политика наивысшего приоритета имеет самые строгие потребности распределителя.

пример have_altivec():

я включил пример have_altivec() реализация для полноты, просто потому, что это самый короткий и, следовательно, наиболее подходящих для размещения здесь. X86 / x86_64 CPUID один грязный, потому что вы должны поддерживать конкретные способы компилятора написания встроенного ASM. OpenCL один грязный, потому что мы проверяем некоторые ограничения реализации и расширения тоже.

#if HAVE_SETJMP && !(defined(__APPLE__) && defined(__MACH__))
jmp_buf jmpbuf;

void illegal_instruction(int sig) {
   // Bad in general - https://www.securecoding.cert.org/confluence/display/seccode/SIG32-C.+Do+not+call+longjmp%28%29+from+inside+a+signal+handler
   // But actually Ok on this platform in this scenario
   longjmp(jmpbuf, 1);
}
#endif

bool have_altivec()
{
    volatile sig_atomic_t altivec = 0;
#ifdef __APPLE__
    int selectors[2] = { CTL_HW, HW_VECTORUNIT };
    int hasVectorUnit = 0;
    size_t length = sizeof(hasVectorUnit);
    int error = sysctl(selectors, 2, &hasVectorUnit, &length, NULL, 0);
    if (0 == error)
        altivec = (hasVectorUnit != 0);
#elif HAVE_SETJMP_H
    void (*handler) (int sig);
    handler = signal(SIGILL, illegal_instruction);
    if (setjmp(jmpbuf) == 0) {
        asm volatile ("mtspr 256, %0\n\t" "vand %%v0, %%v0, %%v0"::"r" (-1));
        altivec = 1;
    }
    signal(SIGILL, handler);
#endif

    return altivec;
}

вывод

в основном вы не платите штраф за платформы, которые никогда не могут поддерживать реализацию (компилятор не генерирует для них код) и только небольшой штраф (потенциально очень предсказуемый парой CPU test/jmp, если ваш компилятор наполовину приличен при оптимизации) для платформ, которые могут поддерживать что-то, но не делают. Ты платишь. никаких дополнительных затрат для платформ, на которых выполняется реализация первого выбора. Детали тестов среды выполнения различаются в зависимости от рассматриваемой технологии.


вы можете использовать подход на основе политики с шаблонами, как это делает стандартная библиотека для распределителей, компараторов и тому подобное. Каждая реализация имеет класс политики, который определяет computeSome () и computeMore (). Ваш класс Algo принимает политику в качестве параметра и откладывает ее реализацию.

template <class policy_t>
class algo_with_policy_t {
    policy_t policy_;
public:
    algo_with_policy_t() { }
    ~algo_with_policy_t() { }

    void process()
    {
        policy_.computeSome();
        policy_.computeMore();
    }
};

struct altivec_policy_t {
    void computeSome();
    void computeMore();
};

struct sse_policy_t {
    void computeSome();
    void computeMore();
};

struct scalar_policy_t {
    void computeSome();
    void computeMore();
};

// let user select exact implementation
typedef algo_with_policy_t<altivec_policy_t> algo_altivec_t;
typedef algo_with_policy_t<sse_policy_t> algo_sse_t;
typedef algo_with_policy_t<scalar_policy_t> algo_scalar_t;

// let user have default implementation
typedef
#if defined(ALTIVEC)
    algo_altivec_t
#elif defined(SSE)
    algo_sse_t
#else
    algo_scalar_t
#endif
    algo_default_t;

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

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());
    }
}

если накладные расходы виртуальной функции приемлемы, вариант 3 плюс несколько ifdefs кажется хорошим компромиссом IMO. Есть два варианта, которые вы могли бы рассмотреть: один с абстрактным базовым классом, а другой с простой реализацией C в качестве базового класса.

наличие реализации C в качестве базового класса позволяет постепенно добавлять векторные оптимизированные версии, возвращаясь к непекторизованным версиям, как вам угодно, используя абстрактный интерфейс, было бы немного чище читать.

кроме того, наличие отдельных C++ и векторизованных версий вашего класса позволяет легко писать модульные тесты, которые

  • убедитесь, что векторизованный код дает правильный результат (легко испортить это, и векторные плавающие регистры могут иметь разную точность, чем FPU, вызывая разные результаты)
  • сравните производительность C++ против векторизованного. Часто бывает полезно убедиться, что векторизованный код действительно приносит вам пользу. Компиляторы могут создайте очень плотный код C++, который иногда делает так же или лучше, чем векторизованный код.

вот один с реализациями plain-c++ в качестве базового класса. Добавление абстрактного интерфейса просто добавит общий базовый класс ко всем трем из них:

// 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
...

вы можете рассмотреть возможность использования шаблонов адаптеров. Существует несколько типов адаптеров, и это довольно расширяемая концепция. Вот интересная статья структурные картины: переходника и Façade это обсуждает очень похожий вопрос на тот, который в вашем вопросе-структура ускорения в качестве примера паттера адаптера.

Я думаю, что неплохо обсудить решение на уровне шаблонов проектирования, не фокусируясь на деталях реализации, таких как язык C++. После того, как вы решите, что адаптер указывает правильное решение для вас, вы можете искать варианты, специфичные для вашей имплементации. Например, в мире C++ известен вариант адаптера, называемый generic adapter pattern.


Это на самом деле не весь ответ: просто вариант одного из ваших существующих вариантов. В варианте 1 Вы предположили, что включаете algo_altivec.cpp &c. в Альго.cpp, но вы не должны этого делать. Ты можешь опустить Альго.cpp полностью, и ваша система сборки решает, какой из algo_altivec.cpp, algo_sse.СРР, и C. для того чтобы построить. Вы должны были бы сделать что-то подобное в любом случае, какой бы вариант вы ни использовали, так как каждая платформа не может скомпилировать каждую реализацию; мое предложение заключается только в том, что какой бы вариант вы выбираете, вместо того, чтобы иметь #if ALTIVEC_ENABLED везде в источнике, где ALTIVEC_ENABLED устанавливается из системы сборки, у вас просто есть система сборки, непосредственно решающая, компилировать ли algo_altivec.СРР. Это немного сложнее достичь в MSVC, чем make, scons, &c., но все еще возможно. Это обычное дело для переключения в целом каталоге, а не отдельных исходных файлов; то есть вместо algo_altivec.cpp и друзья, у вас будет платформа / altivec / algo.cpp, platform/sse / algo.cpp, и так один. Таким образом, когда у вас есть второй алгоритм, для которого вам нужны реализации для конкретной платформы, вы можете просто добавить дополнительный исходный файл в каждый каталог.

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


для того, чтобы скрыть детали реализации, вы можете просто использовать абстрактный интерфейс со статическим творец и 3 классы реализации:

// --------------------- Algo.h ---------------------
#pragma once

typedef boost::shared_ptr<class Algo> AlgoPtr;

class Algo
{
public:
    static AlgoPtr Create(std::string type);
    ~Algo();

    void process();

protected:
    virtual void computeSome() = 0;
    virtual void computeMore() = 0;
};

// --------------------- Algo.cpp --------------------- 
class PlainAlgo: public Algo { ... };
class AltivecAlgo: public Algo { ... };
class SSEAlgo: public Algo { ... };

static AlgoPtr Algo::Create(std::string type) { /* Factory implementation */ }

обратите внимание, что поскольку классы PlainAlgo, AlivecAlgo и SSEAlgo определены в Algo.cpp, они видны только из этого блока компиляции и, следовательно, детали реализации скрыты от внешнего мира.

вот как можно использовать ваш класс:

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

Мне кажется, что ваша первая стратегия с отдельными файлами C++ и#, включая конкретную реализацию, является самой простой и чистой. Я бы только добавил несколько комментариев к вашему Algo.cpp, указывающий, какие методы находятся в файлах #included.
например,

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

Шаблоны, подобные политике (mixins), прекрасны до тех пор, пока требование чтобы вернуться к реализации по умолчанию. Это runtime opeation и должно обрабатываться полиморфизмом времени выполнения. шаблон стратегии могу справиться с этим хорошо.

есть один недостаток такого подхода: Strategy-подобный алгоритм не может быть встроен. Такой inlining может обеспечить разумное улучшение представления в редких случаях. Если это проблема, вам нужно будет охватить более высокий уровень логика Strategy.