使用模板元编程的不同循环展开方法的优点/缺点

时间:2015-08-11 23:12:51

标签: c++ metaprogramming loop-unrolling

我对编译时循环展开的一般解决方案感兴趣(我在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

如果使用匿名lambda,则必须明确指定函数的类型,这很尴尬。

方法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); 

在语法方面,第三种方法看起来最干净,最清晰,但我想知道编译器如何处理三种方法可能存在差异。

1 个答案:

答案 0 :(得分:5)

首先,编译器在展开循环时很容易知道。也就是说,我并不建议明确展开循环。另一方面,索引可以用作类型映射的索引,在这种情况下,有必要展开东西以生成具有不同类型的版本。

我个人的方法是避免递归,而是通过索引扩展来处理展开。这是一个很好地调用和使用的版本的简单演示。传递参数数量的相同技术可以与示例中的递归方法一起使用。我认为这种符号更可取:

#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);
}