У нас есть некоторая шаблонная функция, которая принимает 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, я бы посчитал это сарказмом.