4

コードはオンザフライで記述され、名前の規則が変更されているため、混乱が生じた場合は申し訳ありません。わかりやすくするために、ここで質問を書き直します。

コンパイル時に既知のデータがいくつかあります。整数の 2 つの配列DEで、どちらも長さがLです。の各要素Dは 0 または 1 です。の各要素にEは の値が含まれます[0,L]

X次に、実行時に既知であり、これも lengthのベクトルを取得しましたL

DEおよびを使用して特定の値を計算する関数を作成したいと思いますX。たとえば、次のようになります。

int comp_rt(int i, array<int, L> X) {
    int v = 0;
    if (D[i] == 0) // D[i] known at compile-time
        return 10;
    for (int j = 0; j < E[i]; ++j) // E[i] known at compile-time
        v += X[j] * (j + 1); // X[j] known at run-time
    return v;
}

この計算は何度も実行されるため、オーバーヘッドを削減したいと考えており、コンパイル時にチェックとループを実行できれば素晴らしいと考えましDE

通常、comp_rt関数を使用する代わりに高速化するために、一般的なケースですが、テンプレートごとiに計算を行うだけの特殊な関数を作成します。例えば:

N = 5
D = [0, 1, 1, 0, 1] // Values in {0, 1}
E = [1, 0, 3, 2, 4] // Values in [0, L-1]
X = [1, 3, 5, 7, 9] // Any integer

template <int i> int comp(array<int, L> X);
template <> int comp_tpl<0>(array<int, L> X) { return 10; } // D[0] == 0
template <> int comp_tpl<1>(array<int, L> X) { return 0; } // E[1] == 0, skip loop
template <> int comp_tpl<2>(array<int, L> X) { return X[0] + 2 * X[1] + 3 * X[2]; }
template <> int comp_tpl<3>(array<int, L> X) { return 10; }
template <> int comp_tpl<4>(array<int, L> X) { return compl_tpl<2>(X) + 4 * X[3]; }

私の質問は: テンプレートや定数式を使用して、コンパイル時Dに andを使用して関数を構築することは可能ですか? 「実行時に計算される式を構築する」ものを構築することを意味し、関連する計算のみが実行時に残されます。Ecomp_tplX

また、可能であれば、どのように行われますか? そのような問題を解決するためにどの一般原則を使用できますか?

テンプレートを使用してそれを実行しようとしましたが、結果のコードはそれほど高速ではありませんcomp_tpl...実行時に評価されると思われる再帰呼び出しがいくつかあります。

4

4 に答える 4

2

編集: 問題の説明に従って更新:
Edit2 : を削除しましたConditional

これは以前と同じように合計を計算します。末尾再帰です* :

template<class T, size_t Length> struct Sum {
    template<class Array>
    static T comp(const Array &x, T add = 0) {
        return Sum<T, Length - 1>::comp(x, add + Length * x[Length - 1]);
    }
};

template<class T> struct Sum<T, 0> {
    template<class Array>
    static T comp(const Array &x, T add = 0) {
        return add;
    }
};

これは、それをまとめて と に依存する部分dですe。おそらくそれらをパラメーター化できますが、それは価値があるよりも面倒だと思います。

constexpr int d[] = { 0, 1, 1, 0, 1 };
constexpr int e[] = { 1, 0, 3, 2, 4 };

template<int N> struct Comp {
    template<class Array>
    static int comp(const Array &x) {
        return d[N] ? Sum<int, e[N]>::comp(x) : 10;
    }
};

使用法:

int x[] = { 1, 3, 5, 7, 9 };
Comp<3>::comp(x);

http://ideone.com/PmFBhU

(*) そうではありませんが、十分に近いです。

于 2013-11-06T22:17:58.177 に答える
1

(更新:clang ++とg ++を使用したタイミング実験については最後に説明します。また、簡単にするためcomp_rtに、質問で正確な本体を使用して、関数の本体を書き直す必要なく完全に最適化できることを示しています。)

はい、これは可能です。しかし、g++ は、あなたが気付かないうちに、とにかく多くのことを行っているようです。最後の実験を参照してください。ただし、clang++ を使用すると、ランタイム バージョンが遅くなることが実際にわかります。

以下のプログラムでは、 を除くすべてのパラメータXがテンプレート引数として渡されます。したがって、comp_rt使用するパラメーターの組み合わせごとに異なるテンプレート関数が作成されます。が大きい場合、これによりバイナリが大きくなる可能性がありますL

私の扱い方はD[i]==0、最初はわかりにくいかもしれません。の中に入れましたenable_ifcomp_tplここには 2 つの定義がありD[i]==0ますD[i]==1。正直なところ、これはおそらく不要です。単一のcomp_rt関数テンプレート内で関数の元の本体を使用しただけでも、コードは最適にコンパイルされるのではないかと思います。 (私はこの複雑さを取り除きました)。

関数内に次のような行を含めました。

    using confirm_Ei_is_known_at_compile_time = array<char,E[i]>;

E[i]これにより、コンパイル時に がコンパイラに認識されていることが確認されます。これは typedef と同等であり、 の要素数はarrayコンパイル時にわかっている必要があります。たとえば、 のX[i]代わりにを使用しようとすると、コンパイラはコードを拒否しますE[i]array注: この行は何もしません。コンパイル時のサニティ チェックです。

最後に、それE[i]がコンパイル時にわかっている場合、コンパイラはループをアンロールできます (賢明な判断で、それによって速度が向上すると思われる場合)。必ずすべての最適化をオンにしてください。gcc にはオプションがあります-funroll-all-loops

関連するパラメーターをテンプレート パラメーターとして渡すことにより、コンパイラはより多くの最適化を行うことができます。しかし、それを選択するかどうかはわかりません! 実験が必要です。


これが、タイミング実験に使用した完全なプログラムです。

#include<array>
#include<iostream>
using namespace std;

/*
 * L is a positive integer
 * D is vector of booleans of length L
 * E is a vector of ints [0,L) of length L
 * i will be in [0,L) also, therefore it is small enough that we can
 *         treat it as if it's known at compile time also
 *
 * The only thing that is *not* known at compile time is:
 * X is a vector of ints of length L
 *
 * Therefore, our goal is something like:
 *
 *   template<int L, int i, int D[L], int E[L]>
 *   int compute(int X[L]);
 */

template<int L, int i, const bool (&D)[L], const int (&E)[L]> // arrays passed, by reference, at compile-time
typename enable_if< D[i]==0 , int> :: type
comp_tpl(int (&)[L]) {
        return 10;
}
template<int L, int i, const bool (&D)[L], const int (&E)[L]> // arrays passed, by reference, at compile-time
typename enable_if< D[i]==1 , int> :: type
comp_tpl(int (&X)[L]) {
    int v = 0;
    //using confirm_Ei_is_known_at_compile_time = array<char,E[i]>;
    for (int j = 0; j < E[i]; ++j) // E[i] known at compile-time
        v += X[j] * (j + 1); // X[j] known at run-time
    return v;
}

template<int L, int i, const bool (&D)[L], const int (&E)[L]> // arrays passed, by reference, at compile-time
int
comp_tpl_simple(int (&X)[L]) {
    if (D[i] == 0) // D[i] known at compile-time
        return 10;
    int v = 0;
    using confirm_Ei_is_known_at_compile_time = array<char,E[i]>;
    for (int j = 0; j < E[i]; ++j) // E[i] known at compile-time
        v += X[j] * (j + 1); // X[j] known at run-time
    return v;
}

template<int L> // arrays passed, by reference, at compile-time
int
comp_rt(int i, const bool (&D)[L], const int (&E)[L], int (&X)[L]) {
    if (D[i] == 0) // D[i] known at compile-time
        return 10;
    int v = 0;
    for (int j = 0; j < E[i]; ++j) // E[i] known at compile-time
        v += X[j] * (j + 1); // X[j] known at run-time
    return v;
}


constexpr int L = 5;
extern constexpr bool D[L] {0, 1, 1, 0, 1};  // Values in {0, 1}
extern constexpr int  E[L] {1, 0, 3, 2, 4}; // Values in [0, L-1]

void change_X_arbitrarily(int (&X)[L]) {
    for(int j=0; j<L; ++j)
        ++X[j];
}

int main() {
    int X[L] {1, 3, 5, 7, 9}; // Any integer

#ifdef USE_RUNTIME
    #define comp(L,i,D,E,X) comp_rt<L>(i,D,E,X)
#endif
#ifdef USE_TEMPLATE
    #define comp(L,i,D,E,X) comp_tpl_simple<L,i,D,E>(X)
#endif

    int total=0;
    for(int outer_reps=0; outer_reps<10000; ++outer_reps) {
        for(int inner_reps=0; inner_reps<100000; ++inner_reps) {
            total += comp(L,0,D,E,X);
            total += comp(L,1,D,E,X);
            total += comp(L,2,D,E,X);
            total += comp(L,3,D,E,X);
            total += comp(L,4,D,E,X);
        }
        change_X_arbitrarily(X);
    }

    cout << total << endl; // should be 39798784
}

#define使用する関数を選択するために a を使用する方法に注意してください。私はコンパイルして実行します:

$ clang++ SO.cpp -std=gnu++0x -O3 -DUSE_TEMPLATE -o SO && time -p ./SO
39798784  // the total value from all the calls, as a check
real 0.00
user 0.00
sys 0.00

1,000,000,000 回の計算にかかる時間は 0 秒です。ただし、ランタイム バージョンは 2.7 秒かかります

$ clang++ SO.cpp -std=gnu++0x -O3 -DUSE_RUNTIME -o SO && time -p ./SO
39798784  // the total value from all the calls, as a check
real 2.70
user 2.68
sys 0.00

そこでclang3.3を使用し-O3ました。

g++ 4.8.2 を使用すると、-O3 で未定義の動作に関する警告が表示されますが、奇妙なことに、ランタイム バージョンまたはテンプレート バージョンのいずれかでランタイムが 0 秒です! おそらく g++ は、「ランタイム」モードであっても、コンパイル時のトリックを有効にしています。ここでの教訓は、コンパイラは私たちよりも最適化について多くのことを知っているということです!

とにかく、フォールバックするとg++-4.8.2 -O2、実行時間はどちらの場合も 6.8 秒です。かなり奇妙です!さらに追加すると、O速度が低下することがあります。

説明:この場合、X実際にはコンパイル時に認識されます。これはこのコードのローカル変数であり、決定論的に更新されるため、コンパイラはそれを完全に予測でき、コンパイル時に答えを計算します! g++ がこれを行っているようです (非常に印象的です!)。したがって、私の最新の実験では、グローバル変数のXmaincomp_tpl一貫して現在よりもはるかに高速ですcomp_rt

于 2013-11-08T23:47:09.177 に答える
1

(別の回答を追加して申し訳ありません)

(最後にサンプルコードを載せておきます)

私の実験とさらなる考察の結果、元のコードはわずかな変更を加えるだけでそのまま使用できることがわかりました。コンパイラは最適化を行うのが非常に得意なので、速度を落とすのが難しい場合があります。volatile とマークするXか、からのランダムなデータで常に編集することによってのみrand()、「ランタイム」バージョンを本当に遅くすることができます。

まず、ベクトルが 1 つしかなくD、ベクトルが 1 つしかない場合は、配列宣言の前にE配置するだけです。constexpr

constexpr int D[] = { 0, 1, 1, 0, 1 };
constexpr int E[] = { 1, 0, 3, 2, 4 };

(そのようなベクトルが複数あり、それぞれに「事前に部分的にコンパイルされた」関数を準備したい場合は、他の(長ったらしい)回答で説明したように、テンプレートパラメーターを介してそれらを渡すことができます。)

i元の関数のインデックスも処理する必要があります: int comp_rt(int i, array<int, L> X);. これはテンプレート パラメータである必要があります。

template<size_t i>
int comp_rt(array<int, L> X);

関数の本体を変更する必要はありません。コンパイラはi、 、D[i]、およびE[i]が定数式であることを認識するようになりました。D[i]とを含む式E[i]は、その定数値に置き換えられます。テストif(D[i]==0)は、コンパイル時に、if (true)またはif (false)必要に応じて置き換えられます。また、コンパイラは の値を正確に認識しているため、ループは展開されますE[i]。ループが展開され、コンパイラはそれを確認できますv単純に高額です。この場合、それを明示的な合計に置き換え、すべてのゼロ項を削除し、すべての定数項を合計します。これらはすべてコンパイル時に行われます。ここでコンパイラを支援するためにできることはほとんどありません。これは、使用できるより複雑なテンプレート ソリューションの一部と同等です。

-O2 および -O3 で g++ および clang++ を使用しました。

私の実験のいくつかでは、gcc必要な反復回数に関係なく、 のプログラムは 0 秒で実行されました。Xこれは、アルゴリズムが決定論的であり、gcc が(X を定期的に変更していたとしても!)発生するすべてのことを事前に計算できるためです。その場合、問題の一部は私がXローカル変数を作成したことでしたが、教訓は、コンパイラーが決定論的プログラムを見て、すべてを事前に計算できることです。 clangここでは積極的に最適化していないようです。

予想よりも遅いコードがあり、遅いコードを示す完全なコードをまとめることができる場合は、おそらく他の小さな変更を提案できます。しかし、constexpron data と のテンプレート パラメーターを使用iするだけでうまくいくと思います。


このサンプル コードでは、別の変更を加えています。tuple_size<array<char, D[i]> > :: value単に の代わりに間接的に使用D_iしています。これは実際には意味を変えませんが、古いコンパイラがコンパイル時に評価することを奨励すると思います。これでの私の目標は、コードの元の可読性を可能な限り一致させ、たとえばこの関数全体を多くのテンプレートに分割するのではなく、1 つの場所にまとめることです。

constexpr int L   = 5;
constexpr int D[] = { 0, 1, 1, 0, 1 };
constexpr int E[] = { 1, 0, 3, 2, 4 };
template<int i>
int comp_rt(array<int, L> X) {
    using D_i_type = array<char, D[i]>;
    int v = 0;
    if (tuple_size<D_i_type>::value == 0) // D[i] known at compile-time
        return 10;
    using E_i_type = array<char, E[i]>;
    for (int j = 0; j < tuple_size<E_i_type>::value; ++j) { // E[i] known at compile-time
        v += X[j] * (j + 1); // X[j] known at run-time
    }
    return v;
}
于 2013-11-09T20:59:39.027 に答える
-1

constexpr 関数を使用すると、コンパイル時に実行できます

#include <iostream>
#include <array>
#include <utility>

#include <cstddef>
#include <type_traits>

  /// A type that represents a parameter pack of zero or more integers.
  template<typename T, T... I>
    struct integer_sequence
    {
      static_assert( std::is_integral<T>::value, "Integral type" );

      using type = T;

      static constexpr T size = sizeof...(I);

      /// Generate an integer_sequence with an additional element.
      template<T N>
        using append = integer_sequence<T, I..., N>;

      using next = append<size>;
    };

  template<typename T, T... I>
    constexpr T integer_sequence<T, I...>::size;

  template<std::size_t... I>
    using index_sequence = integer_sequence<std::size_t, I...>;

  namespace detail
  {
    // Metafunction that generates an integer_sequence of T containing [0, N)
    template<typename T, T Nt, std::size_t N>
      struct iota
      {
        static_assert( Nt >= 0, "N cannot be negative" );

        using type = typename iota<T, Nt-1, N-1>::type::next;
      };

    // Terminal case of the recursive metafunction.
    template<typename T, T Nt>
      struct iota<T, Nt, 0ul>
      {
        using type = integer_sequence<T>;
      };
  }


  // make_integer_sequence<T, N> is an alias for integer_sequence<T, 0,...N-1>
  template<typename T, T N>
    using make_integer_sequence = typename detail::iota<T, N, N>::type;

  template<int N>
    using make_index_sequence = make_integer_sequence<std::size_t, N>;


  // index_sequence_for<A, B, C> is an alias for index_sequence<0, 1, 2>
  template<typename... Args>
    using index_sequence_for = make_index_sequence<sizeof...(Args)>;
//--------------------My part starts here-------------------------------------------------------
template <size_t N> constexpr int computebis(int bound,std::array<int,N> X,int j)
{
  return (j<bound) ? X[j]*(j+1) + computebis(bound,X,j+1) : 0;
}

template <size_t N> constexpr int compute2(std::array<int,N> D,
                                            std::array<int,N> E,
                                            std::array<int,N> X,int index)
{
  return (D[index]==0) ? 10 : computebis(E[index],X,0);
}


template <size_t N,std::size_t... Indices> constexpr std::array<int,N> mfill(std::array<int,N> D,
                                                                            std::array<int,N> E,
                                                                            std::array<int,N> X,
                                                                            index_sequence<Indices...>)
{
  return {{ compute2(D,E,X,Indices)... }};
}

template <size_t N> constexpr std::array<int,N> mfill(std::array<int,N> D,std::array<int,N> E,std::array<int,N> X)
{
  return mfill(D,E,X,make_index_sequence<N>{});
}


int main(int argc, char *argv[])
{

  std::array<int,5> D= {0,1,1,0,1};
  std::array<int,5> E= {1,0,3,2,4};
  std::array<int,5> X= {1,3,5,7,9};
  //to be sure that it is done at compil time
  const auto X2 =  mfill(D,E,X);

  for(auto e:X2){
    std::cout<<e<<std::endl;
  }

編集: C ++ 11でN要素constexpr配列を作成することに触発されたコードが更新され 、最初の部分はすべてそこに取りました

于 2013-11-06T22:25:43.053 に答える