Вложенные операторы переключения для аргументов шаблона

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

«Есть ли способ сделать это немного лучше…» Слово «приятнее» означает разные вещи для разных людей. Определите, что вы подразумеваете под словом «лучше» в языке программирования C++. Итак, мнение основано. Точно так же, как «Замечательные люди на ТАК...» не обязательно хорошо. Кто-то может счесть это сарказмом. И, будучи опытным пользователем SO, я бы посчитал это сарказмом.

user12002570 17.07.2024 11:15

По соображениям производительности лучше иметь специализированные реализации этой функции, основанные на некотором предопределенном наборе значений a и b. Мой опыт таков: если это правда, скорее всего, ваш компилятор уже делает это. Проверьте, действительно ли шаблонная функция работает быстрее. Возможно, вы сэкономите себе много работы.

Marcus Müller 17.07.2024 11:17

Это похоже на микрооптимизацию, особенно время и усилия, которые нужно потратить на правильную обработку всех 10 реализаций.

user12002570 17.07.2024 11:17

Вы можете заменить все функции auto faXX(int B) шаблонной функцией template <int A> auto fa<A>(int B), при этом все возвращаемые значения будут изменены на return SomeFunc<A, number from switch>;.

mch 17.07.2024 11:17

@user12002570 user12002570 по умолчанию не предполагает сарказма. В этом и заключается боль при общении с незнакомцами! Но то, что «Замечательные люди на SO» определенно является отвлечением, не имеющим отношения к вопросу – и вам разрешено улучшать вопросы;)

Marcus Müller 17.07.2024 11:18

@MarcusMüller Я проверил, это быстрее. Точнее, внутренние вычисления с использованием встроенных функций OpenMP SIMD действительно выполняются быстрее, если значения известны во время компиляции. Я сравнивал это снова и снова.

debronee101 17.07.2024 11:18

@debronee101 спасибо! да, с SIMD, оптимизированным вручную, это может быть так (см. libvolk.org, в котором я слегка участвую).

Marcus Müller 17.07.2024 11:21

Вы можете использовать std::make_index_sequence для создания диапазона (0..max_A * max_B), построить массив указателей функций в форме SomeFunc<N/max_B,N%max_B> и, наконец, индексировать этот массив во время выполнения, используя a * max_b + b?

Botje 17.07.2024 11:23

Меня интересует одна вещь: откуда берутся a и b во время выполнения? Не могли бы вы проиллюстрировать, как это будет выглядеть там, где вы используете эту функцию?

Marcus Müller 17.07.2024 11:26

@MarcusMüller a и b по сути являются конкретными размерностями матрицы. Они исходят от пользователя, что зависит от рассматриваемого приложения. Должен ли я добавить эту информацию в ответ или вы думаете, что достаточно предположить это неявно?

debronee101 17.07.2024 11:28

@user12002570 user12002570 Вы не совсем ошибаетесь, говоря, что это микрооптимизация. Тем не менее, поскольку SomeFunc вызывается повторно и внутренне использует встроенные функции SIMD (векторизацию), вы действительно получаете значительную производительность, выполняя такого рода оптимизации. Аргументы времени компиляции, заполнение, выравнивание и т. д. Конечно, это некрасиво, но реализовать это нужно только один раз.

debronee101 17.07.2024 11:41
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
11
90
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

#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[]. Спасибо

debronee101 17.07.2024 11:58

Предполагая, что желаемый диапазон 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 17.07.2024 11:54

@debronee101 самая внутренняя лямбда создает std::array<FuncPtr, NumB> путем расширения пакета 0, 1, ... NumB-1, который создается std::make_integer_sequence. В основе этого лежит еще одно расширение для As. Для полностью прерывистых значений вы можете использовать std::map<int, FuncPtr>, который можно заполнить аналогичным образом.

Caleth 17.07.2024 14:04
Ответ принят как подходящий

Один из способов превратить значение времени выполнения в значение времени компиляции — использовать 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, если хотите.

Другие вопросы по теме