Как и было запрошено в комментариях, вот краткое изложение того, что я сделал:
Настройка 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 процессора, если ваш компилятор наполовину приличен в оптимизации) для платформ, которые могут что-то поддерживать, но не поддерживают.Вы не платите дополнительную плату за платформы, на которых работает реализация первого выбора.Детали тестов во время выполнения варьируются в зависимости от рассматриваемой технологии.