3

コンパイル時のループ展開の一般的なソリューションに興味があります (各関数呼び出しが特定の数のクロック サイクルを取り、複数の呼び出しを並行して実行できる 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...);

これまでのところ、私は 3 つの異なるテンプレート メタプログラム ソリューションを使用しており、特にコンパイラが関数呼び出しをインライン化する方法に関して、異なるアプローチの利点/欠点は何か疑問に思っています。

アプローチ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); 

構文に関しては、この 3 番目のアプローチが最もクリーンで明確に見えますが、コンパイラによる 3 つのアプローチの処理方法に違いがあるのではないかと考えています。

4

1 に答える 1

6

まず第一に、コンパイラはループを展開するタイミングをよく知っている傾向があります。つまり、明示的にループを展開することを提案しているわけではありません。一方、インデックスはタイプマップへのインデックスとして使用できます。その場合、さまざまなタイプのバージョンを生成するために物事を展開する必要があります。

ただし、私の個人的なアプローチは、再帰を回避し、展開をインデックス展開で処理することです。これは、適切に呼び出されて使用されるバージョンの簡単なデモです。例のように、引数の数を渡す同じ手法を再帰的アプローチで使用できます。表記が好ましいと思います:

#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);
}
于 2015-08-12T00:29:27.283 に答える