У нас есть некоторая шаблонная функция, которая принимает 2 аргумента времени компиляции, например.
template<int a, int b>
void SomeFunc(double *x, double *y, double *z);
По соображениям производительности лучше иметь специализированные реализации этой функции, основанные на некотором предопределенном наборе значений для a и b. Разумное количество значений для каждого — 10, таким образом, всего возможно не более 100 реализаций SomeFunc. Если вам интересно, почему, то это потому, что для рассматриваемого приложения знание этих значений во время компиляции позволяет компилятору лучше оптимизировать SomeFunc, особенно потому, что оно включает в себя тяжелые плотные вычисления.
Один из способов обойти эту проблему — иметь два вложенных оператора переключения и оценивать их во время выполнения каждый раз, когда вызывается функция. Однако это одновременно и избыточно, и приводит к ненужной повторной оценке одних и тех же операторов переключения каждый раз (поскольку значения a и b не меняются).
Другой подход может заключаться в использовании указателя на функцию, который предварительно обрабатывает значения a и b и решает, на какую функцию указывать. В этом случае SomeFunc достаточно велик, так что мне не нужно беспокоиться о его встраивании и мне не придется платить здесь за производительность. Тем не менее, проблема в том, что нам придется написать эти два утомительных вложенных оператора переключения, например
auto DecideFuncPtr(int A, int B)
{
switch(A)
{
case(1): return fa1(B);
case(2): return fa2(B);
...
}
}
каждая специализированная реализация выглядит так:
// A = 1
auto fa1(int B)
{
switch(B)
{
case(1): return SomeFunc<1,1>;
case(2): return SomeFunc<1,2>;
...
}
}
// A = 2
auto fa2(int B)
{
switch(B)
{
case(1): return SomeFunc<2,1>;
case(2): return SomeFunc<2,2>;
...
}
}
...
И, наконец, предварительная обработка соответствующей функции один раз (например, в конструкторе) как таковая:
auto f = DecideFuncPtr(A, B);
// Use the function later on as such:
f(matrix_A, matrix_B, matrix_C);
Таким образом, вы можете увидеть, насколько неряшливо это может выглядеть с точки зрения дизайна. В частности, если нужно добавить/откорректировать комбинацию реализуемых значений a и b. Есть ли способ сделать это немного лучше, при этом гарантируя, что результирующая функция SomeFunc имеет значения a и b, известные во время компиляции?
Для завершения я могу использовать C++17 или даже C++20.
Обновлено: я забыл добавить тип функции (void); это не меняет псевдокод и поставленный вопрос. Также обратите внимание, что значения a и b не обязательно являются последовательными. Здесь я указываю значения 1 и 2 только в демонстрационных целях.
По соображениям производительности лучше иметь специализированные реализации этой функции, основанные на некотором предопределенном наборе значений a и b. Мой опыт таков: если это правда, скорее всего, ваш компилятор уже делает это. Проверьте, действительно ли шаблонная функция работает быстрее. Возможно, вы сэкономите себе много работы.
Это похоже на микрооптимизацию, особенно время и усилия, которые нужно потратить на правильную обработку всех 10 реализаций.
Вы можете заменить все функции auto faXX(int B) шаблонной функцией template <int A> auto fa<A>(int B), при этом все возвращаемые значения будут изменены на return SomeFunc<A, number from switch>;.
@user12002570 user12002570 по умолчанию не предполагает сарказма. В этом и заключается боль при общении с незнакомцами! Но то, что «Замечательные люди на SO» определенно является отвлечением, не имеющим отношения к вопросу – и вам разрешено улучшать вопросы;)
@MarcusMüller Я проверил, это быстрее. Точнее, внутренние вычисления с использованием встроенных функций OpenMP SIMD действительно выполняются быстрее, если значения известны во время компиляции. Я сравнивал это снова и снова.
@debronee101 спасибо! да, с SIMD, оптимизированным вручную, это может быть так (см. libvolk.org, в котором я слегка участвую).
Вы можете использовать std::make_index_sequence для создания диапазона (0..max_A * max_B), построить массив указателей функций в форме SomeFunc<N/max_B,N%max_B> и, наконец, индексировать этот массив во время выполнения, используя a * max_b + b?
Меня интересует одна вещь: откуда берутся a и b во время выполнения? Не могли бы вы проиллюстрировать, как это будет выглядеть там, где вы используете эту функцию?
@MarcusMüller a и b по сути являются конкретными размерностями матрицы. Они исходят от пользователя, что зависит от рассматриваемого приложения. Должен ли я добавить эту информацию в ответ или вы думаете, что достаточно предположить это неявно?
@user12002570 user12002570 Вы не совсем ошибаетесь, говоря, что это микрооптимизация. Тем не менее, поскольку SomeFunc вызывается повторно и внутренне использует встроенные функции SIMD (векторизацию), вы действительно получаете значительную производительность, выполняя такого рода оптимизации. Аргументы времени компиляции, заполнение, выравнивание и т. д. Конечно, это некрасиво, но реализовать это нужно только один раз.





Возможно, вы могли бы использовать таблицу указателей функций, например:
#include <iostream>
typedef double (*SomeFunc)(double*, double*, double*);
template<int a, int b>
auto f1(double* x, double* y, double* z) {
return a + b + *x + *y + *z;
}
SomeFunc table[] = {
f1<1, 1>,
f1<1, 2>,
f1<2, 1>,
f1<2, 2>,
};
auto DecideFuncPtr(int A, int B) {
return table[(A-1)*2 + B-1];
}
int main(int argc, const char * argv[]) {
double x = 1, y = 3, z = 5;
for (int a = 1; a < 3; ++a)
for (int b = 1; b < 3; ++b)
std::cout << DecideFuncPtr(a, b)(&x, &y, &z) << std::endl;
return 0;
}
Я забыл явно определить тип функции SomeFunc. Это void не double. В любом случае, я думаю, что ваш подход может быть немного лучше, поскольку он включает в себя группировку всего уродливого синтаксиса в table[]. Спасибо
Предполагая, что желаемый диапазон a и b является смежным или вы не против создания экземпляров дыр, вы можете создать (2d) массив указателей на функции, а затем индексировать его во время выполнения.
auto DecideFuncPtr(int A, int B) {
static constexpr auto FuncPtrs = [=]<int... Xs>(std::integer_sequence<int, Xs...>) {
return std::array{
[=]<int X, int...Ys>(std::integral_constant<int, X>, std::integer_sequence<int, Ys...>) {
return std::array<FuncPtr, MaxB - MinB>{ &SomeFunc<MinA + X, MinB + Ys>... };
}(std::integral_constant<int, Xs>(), std::make_integer_sequence<int, MaxB - MinB>())... };
}(std::make_integer_sequence<int, MaxA - MinA>());
return FuncPtrs[A - MinA][B - MinB];
}
Если вас устраивают 0, то MinA и MinB вам не нужны.
С другой стороны, если значения находятся повсюду, лучше использовать поиск по карте.
constexpr std::integer_sequence<int, 2, 5, 9, 42> As;
constexpr std::integer_sequence<int, 4, 18, 21, 69> Bs;
auto DecideFuncPtr(int A, int B) {
static const auto FuncPtrs = [=]<int... Xs>(std::integer_sequence<int, Xs...>) {
std::map<std::pair<int, int>, FuncPtr> result;
(result.merge([=]<int X, int...Ys>(std::integral_constant<int, X>, std::integer_sequence<int, Ys...>) {
return std::map<std::pair<int, int>, FuncPtr>{ { {X, Ys}, &SomeFunc<X, Ys> }... };
}(std::integral_constant<int, Xs>(), Bs)), ...);
return result;
}(As);
return FuncPtrs.at({A, B});
}
Где числа в типах As и Bs — это значения, которые у вас есть.
Соседние значения в a и b предназначены только для демонстрации. Они не обязательно смежны. Тем не менее, я думаю, мне придется провести остаток дня, пытаясь расшифровать то, что вы здесь сделали (я имею в виду это в хорошем смысле).
@debronee101 самая внутренняя лямбда создает std::array<FuncPtr, NumB> путем расширения пакета 0, 1, ... NumB-1, который создается std::make_integer_sequence. В основе этого лежит еще одно расширение для As. Для полностью прерывистых значений вы можете использовать std::map<int, FuncPtr>, который можно заполнить аналогичным образом.
Один из способов превратить значение времени выполнения в значение времени компиляции — использовать std::variant:
std::variant<
std::integral_constant<enumA, enumA::Value0>,
std::integral_constant<enumA, enumA::Value1>,
std::integral_constant<enumA, enumA::Value2>
// ...
> to_variant(enumA a) {
switch (a) {
case enumA::Value0: return std::integral_constant<enumA, enumA::Value0>{};
case enumA::Value1: return std::integral_constant<enumA, enumA::Value1>{};
case enumA::Value2: return std::integral_constant<enumA, enumA::Value2>{};
// ...
}
std::unreachable(); // Or throw
}
// Similar for enumB
Вы делаете это один раз по типу. вам не нужно самостоятельно вычислять декартово произведение.
Затем вы можете использовать std::visit, чтобы сделать всю работу за вас:
auto foo(enumA a, enumB b)
-> void (*)(double*, double*, double*)
{
return std::visit([](auto a, auto b){ return &SomeFunc<a(), b()>; },
to_variant(a), to_variant(b));
}
// or directly
void foo(enumA a, enumB b, double* x, double* y, double* z)
{
std::visit([&](auto a, auto b){ return SomeFunc<a(), b()>(x, y, z); },
to_variant(a), to_variant(b));
}
Примечание. Здесь я использовал enum вместо int, так как это лучше выражает, чем возможные значения «ограничены», но вы можете сделать это с int, если хотите.
«Есть ли способ сделать это немного лучше…» Слово «приятнее» означает разные вещи для разных людей. Определите, что вы подразумеваете под словом «лучше» в языке программирования C++. Итак, мнение основано. Точно так же, как «Замечательные люди на ТАК...» не обязательно хорошо. Кто-то может счесть это сарказмом. И, будучи опытным пользователем SO, я бы посчитал это сарказмом.