Меня интересуют общие решения для развертывания цикла во время компиляции (я использую это в настройке SIMD, где каждый вызов функции занимает определенное количество тактовых циклов, и несколько вызовов могут выполняться параллельно, поэтому мне нужно настроить число аккумуляторов, чтобы минимизировать потраченные впустую циклы — добавление дополнительных аккумуляторов и ручное развертывание приводит к значительным улучшениям, но является трудоемким).
В идеале я хотел бы иметь возможность писать такие вещи, как
unroll<N>(f,args...); // with f a pre-defined function
unroll<N>([](...) { ... },args...); // using a lambda
и сгенерировать следующее:
f(1,args...);
f(2,args...);
...
f(N,args...);
До сих пор у меня есть три различных решения для метапрограмм шаблонов, и мне интересно, каковы преимущества / недостатки различных подходов, особенно относительно того, как компилятор встроит вызовы функций.
Подход 1 (рекурсивная функция)
template <int N> struct _int{ };
template <int N, typename F, typename ...Args>
inline void unroll_f(_int<N>, F&& f, Args&&... args) {
unroll_f(_int<N-1>(),std::forward<F>(f),std::forward<Args>(args)...);
f(N,args...);
}
template <typename F, typename ...Args>
inline void unroll_f(_int<1>, F&& f, Args&&... args) {
f(1,args...);
}
Пример синтаксиса вызова:
int x = 2;
auto mult = [](int n,int x) { std::cout << n*x << " "; };
unroll_f(_int<10>(),mult,x); // also works with anonymous lambda
unroll_f(_int<10>(),mult,2); // same syntax when argument is temporary
Подход 2 (рекурсивный конструктор)
template <int N, typename F, typename ...Args>
struct unroll_c {
unroll_c(F&& f, Args&&... args) {
unroll_c<N-1,F,Args...>(std::forward<F>(f),std::forward<Args>(args)...);
f(N,args...);
};
};
template <typename F, typename ...Args>
struct unroll_c<1,F,Args...> {
unroll_c(F&& f, Args&&... args) {
f(1,args...);
};
};
Синтаксис вызова довольно уродлив:
unroll_c<10,decltype(mult)&,int&>(mult,x);
unroll_c<10,decltype(mult)&,int&>(mult,2); // doesn't compile
и тип функции должен быть указан явно, если используется анонимная лямбда, что неудобно.
Подход 3 (рекурсивная статическая функция-член)
template <int N>
struct unroll_s {
template <typename F, typename ...Args>
static inline void apply(F&& f, Args&&... args) {
unroll_s<N-1>::apply(std::forward<F>(f),std::forward<Args>(args)...);
f(N,args...);
}
// can't use static operator() instead of 'apply'
};
template <>
struct unroll_s<1> {
template <typename F, typename ...Args>
static inline void apply(F&& f, Args&&... args) {
f(1,std::forward<Args>(args)...);
}
};
Пример синтаксиса вызова:
unroll_s<10>::apply(mult,x);
unroll_s<10>::apply(mult,2);
С точки зрения синтаксиса этот третий подход кажется самым чистым и ясным, но мне интересно, могут ли быть различия в том, как эти три подхода обрабатываются компилятором.
Во-первых, компиляторы, как правило, достаточно хорошо знают, когда уместно развернуть циклы. То есть я не предлагаю явно развернуть циклы. С другой стороны, индекс может использоваться как индекс в карте типов, и в этом случае необходимо развернуть вещи для генерации версий с различными типами.
Мой личный подход состоял бы в том, чтобы избежать рекурсии, и, скорее, чтобы развертывание обрабатывалось расширением индекса. Вот простая демонстрация версии, которая хорошо называется и используется. Тот же метод для передачи количества аргументов может быть использован с рекурсивным подходом, как в вашем примере. Я думаю, что обозначение предпочтительнее:
#include <iostream>
#include <utility>
#include <initializer_list>
template <typename T> struct unroll_helper;
template <std::size_t... I>
struct unroll_helper<std::integer_sequence<std::size_t, I...> > {
template <typename F, typename... Args>
static void call(F&& fun, Args&&... args) {
std::initializer_list<int>{(fun(I, args...), 0)...};
}
};
template <int N, typename F, typename... Args>
void unroll(F&& fun, Args&&... args)
{
unroll_helper<std::make_index_sequence<N> >::call(std::forward<F>(fun), std::forward<Args>(args)...);
}
void print(int index, int arg) {
std::cout << "print(" << index << ", " << arg << ")\n";
}
int main()
{
unroll<3>(&print, 17);
}