C ++ обрабатывает специфический impl - #ifdef против частного наследования против отправки тегов - PullRequest
9 голосов
/ 26 сентября 2011

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

1. Различные файлы реализации с сырой включает в себя

У меня тот же заголовочный файл, описывающий интерфейс класса с разными «псевдо» файлы реализации для простого 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 - повсюду

Минусы:

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

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

Плюсы:

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

Минусы:

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

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

Кроме того, я не хочу чрезмерно проектировать вещи, и в этом отношении первый вариант кажется самым «ПОЦЕЛУЮ», как даже учитывая недостатки.

Ответы [ 8 ]

7 голосов
/ 30 сентября 2011

Вы можете использовать подход, основанный на политике, с шаблонами, подобными тем, которые стандартная библиотека делает для распределителей, компараторов и тому подобного.Каждая реализация имеет класс политики, который определяет 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());
    }
}
2 голосов
/ 05 октября 2011

Как и было запрошено в комментариях, вот краткое изложение того, что я сделал:

Настройка policy_list Утилита вспомогательных шаблонов

Это ведет список политик и дает им «время выполнения».check "call перед вызовом первой подходящей реализации

#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.Вы можете сгенерировать это из своего сценария сборки, или использовать серию typedef s, или взломать поддержку "дырок" в policy_list, которые могут быть недействительными на некоторых архитектурах, пропускающих прямо к следующей, без попытки проверкислужба поддержки.GCC устанавливает некоторые макросы препроцессора, которые могут вам помочь, например, __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 конкретный компьютер.Так было проще, хотя в данной политике это может быть typedef, и вы всегда предполагаете, что политика с наивысшим приоритетом имеет самые строгие потребности в распределителях.

Пример have_altivec():

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

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

Заключение

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

2 голосов
/ 26 сентября 2011

Если допустимы издержки на виртуальные функции, вариант 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
...
1 голос
/ 03 октября 2011

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

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

1 голос
/ 28 сентября 2011

Вы можете использовать шаблоны адаптеров.Существует несколько типов адаптеров, и это довольно расширяемая концепция.Вот интересная статья Структурные шаблоны: Адаптер и Фасад , в которой обсуждается вопрос, очень похожий на вопрос в вашем вопросе - Ускорение фреймворка как пример шаблона адаптера.хорошая идея обсудить решение на уровне шаблонов проектирования, не сосредотачиваясь на деталях реализации, таких как язык C ++.Как только вы решите, что адаптер указывает правильное решение для вас, вы можете искать варианты, специфичные для вашей реализации.Например, в мире C ++ существует известный вариант адаптера, называемый универсальным шаблоном адаптера.

0 голосов
/ 05 октября 2011

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

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

0 голосов
/ 01 октября 2011

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

0 голосов
/ 01 октября 2011

Чтобы скрыть детали реализации, вы можете просто использовать абстрактный интерфейс со статическим создателем и предоставить три 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();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...