【问题标题】:building functions at compile time using templates and/or constexpr在编译时使用模板和/或 constexpr 构建函数
【发布时间】:2013-11-18 08:42:15
【问题描述】:

代码是即时编写的,并且会更改名称约定,如果我弄得一团糟,非常抱歉。我将在这里重写问题以使其更清楚。

在编译时有一些已知数据,两个整数数组DE,长度均为LD 的每个元素都是零或一。 E 的每个元素都包含[0,L] 中的一个值。

然后我有一个向量X,它在运行时是已知的,长度也是L

我想构建一个使用DEX 计算某个值的函数,例如:

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]; }

我的问题是:是否可以使用模板和/或常量表达式在编译时使用DE 构建函数,但执行速度与comp_tpl 一样快?我的意思是构建一些“构建要在运行时计算的表达式”的东西,并且只有涉及 X 的计算留给运行时。

而且,如果可能的话,它是如何做到的?哪些一般原则可用于解决此类问题?

我尝试使用模板来执行此操作,但生成的代码不如comp_tpl 快...我认为有些递归调用是在运行时进行评估的。

【问题讨论】:

  • 也许一个简单的预先计算的查找表会是一个不错的选择。
  • 嗯,是的,但问题是关于如何解决这类问题。例如,在尝试求解微分方程时会发现类似的情况:给定一个模型(在编译时),可以从中构建微分方程,然后使用一个函数来计算方程在不同点 (X) 的值经常被调用。我正在尝试概括:)
  • 是否在循环中调用了comp_rt_final,该循环将i 从0 迭代到L-1 并设置X[i]=comp_rt_final(i,X)?如果是这样,我猜comp_rt_final 和这个循环足以准确地指定您的算法。
  • 其实我还有一点意见。我建议您重写您的问题以使其更短。另外,我建议您首先明确说明算法(例如,comp_rt_final 和调用它的循环)。另外,我敦促您不要对编译时优化过于分心 - 在询问优化之前明确说明算法。然后,在那之后,澄清哪些变量在编译时是已知的(D,E,L?),哪些是仅在运行时才知道的输入(X?)。另外,N 是什么?最后,你应该给你的输出变量一个名字(也许是Y?)。
  • 你写的 (int j = 0; j

标签: c++ templates c++11 metaprogramming template-meta-programming


【解决方案1】:

编辑:根据相关说明进行了更新:
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;
    }
};

这是将其组合在一起的部分,取决于de。您可能可以将它们参数化,但我认为这比值得更麻烦。

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

(*) 不是真的,但足够接近。

【讨论】:

  • 嗯,不 :P 您的代码只计算 x[0] + 2 * x[1] + 3 * x[2]...,但在我的代码中,计算了该序列的值取决于两个向量 DE。抱歉,我的代码可能不清楚,因为我没有在 comp_rt 中使用对 D 的依赖,但仍然必须评估 D[i]E[i] 才能构建表达式。
  • 很好,我喜欢这个 :) 你测试它的速度了吗?
  • @AkiRoss 不,除了 ideone 说它在 0 秒内运行;)通常像这样的代码会被优化得很好并且运行得尽可能快,但它确实依赖于尾调用优化实际应用它们的编译器可能会变幻无常(根据我的经验)。
  • 好的,很好。我会尽快检查尾部优化是否能提高性能。我注意到我的暂定解决方案比您的更复杂,因为我没有如此有效地使用constexpr,并且没有尾部优化。谢谢!
  • 终于有时间用这种方式尝试了,实际上我得到了很好的结果,但是我需要打开编译器优化,否则代码不会像手工制作的那样快.
【解决方案2】:

使用 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;
  }

编辑:代码更新 灵感来自Create N-element constexpr array in C++11 我拿走了所有第一部分there

【讨论】:

  • 很有趣,但不是我想要的。有关详细信息,请参阅更新后的问题。
  • 但是...将X 作为 constexpr 中的参数,它在编译时不是必需的吗? X 在运行时已知并且会经常更改。
  • 将函数标记为constexpr 不会强制它在编译时执行。如果 all 参数在编译时已知,它只是对编译器的一个提示,允许它在编译时计算答案。要确认此答案不计算常量表达式,只需将const auto X2 = mfill(D,E,X); 更改为constexpr auto X2 = mfill(D,E,X);,您将得到错误。例如,“注意:在常量表达式中不允许读取非 constexpr 变量 'D'”.
  • ...我也把constexpr放在DEX前面,然后得到这个错误“non-constexpr function 'operator[]'不能在常量表达式中使用”。而且,无论如何,我们不能在这个问题中将 X 作为 constexpr。
【解决方案3】:

(更新:最后讨论了 clang++ 和 g++ 的计时实验。另外,为简单起见,我在问题中使用了 comp_rt 的确切正文,证明它可以完全优化而无需我们重写函数体。)

是的,这可以做到。但是,g++ 似乎在你没有意识到的情况下为你做了很多这样的事情,请参阅最后的实验。但是,使用 clang++,您确实可以看到运行时版本变慢了。

在下面的程序中,除了X 之外的所有参数都作为模板参数传递。因此,将为您使用的每种参数组合构建不同的comp_rt 模板函数。 如果L 很大,这可能会导致您的二进制文件变大。

我传递D[i]==0 的方式一开始可能很难理解。 我把它放在enable_if 中。这里comp_tpl的定义有两种,一种在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 来选择要使用的函数。我编译运行:

$ 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 次需要零秒!但运行时版本需要 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 未定义行为的警告,但奇怪的是,无论是运行时版本还是模板版本,运行时间都是零秒!也许 g++ 为我们启用了编译时技巧,即使在“运行时”模式下也是如此。这里的教训是,编译器确实比我们更了解优化!

无论如何,如果我退回到g++-4.8.2 -O2,那么无论哪种情况,运行时间都是 6.8 秒。很奇怪!有时添加更多 O 会减慢速度!

解释: 在这种情况下,X 实际上在编译时是已知的。它是这段代码中的一个局部变量,并且确定性地更新,因此编译器能够完全预测它并在编译时计算答案!看来 g++ 正在这样做(非常令人印象深刻!)。因此,在我最近的实验中,我将X 移到了main 之外,使其成为一个全局变量,现在优化行为“如预期”。 comp_tpl 现在始终比 comp_rt 快得多。

【讨论】:

  • 嗯,有趣,我的 gcc (4.8.2) 没有做这种优化,即使X 没有改变,函数还是在运行时计算!
  • 我也刚刚安装了 g++ 4.8.2。我注意到只需从const int E[L] = ... 中删除const 就会将运行时间从0 秒更改为11 秒。 (在这两种情况下都使用 comp_rt 和 -O3)这强调了如何声明 D 和 E 的重要性,尤其是 E。constexpr 将是编译器最清晰的提示。它还表明,这里有很多不同的问题在相互作用。也许你不能向我们展示你所有的代码,但你能向我们展示声明吗?您正在测试的函数的声明以及 D 和 E 变量的声明?
【解决方案4】:

(抱歉添加另一个答案)

(我会把示例代码放在最后)

我的实验和进一步的思考使我确信,原始代码只需稍作修改即可按原样使用。编译器非常擅长优化,我发现有时很难放慢速度!只有将X标记为volatile,或者用rand()中的随机数据不断对其进行编辑,才能真正让“运行时”版本变慢。

首先,如果您只有一个D 向量和一个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&lt;int, L&gt; X);。应该是模板参数:

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

函数的主体不需要更改。编译器现在知道iD[i]E[i] 是常量表达式。涉及D[i]E[i] 的表达式被它们的常量值替换。测试if(D[i]==0) 在编译时根据需要被if (true)if (false) 替换。此外,循环将被展开,因为编译器确切地知道E[i] 的值。循环展开,编译器可以看到v 只是一个长和。在这种情况下,它将用显式和替换它,删除所有零项,并将所有常数项相加,依此类推。所有这些都在编译时完成。我们几乎无能为力来帮助这里的编译器。这相当于一些可以使用的更复杂的模板解决方案。

将 g++ 和 clang++ 与 -O2 和 -O3 一起使用。

在我的一些实验中,gcc 的程序在零秒内运行,无论我需要多少次迭代。这是因为该算法是确定性的,并且 gcc 可以提前计算 X 会发生的所有事情(即使我定期更改 X!)。在这种情况下,部分问题在于我将X 设为局部变量,但教训是编译器可以看到确定性程序并提前计算所有内容,即使您不希望这样做! clang 在这里似乎没有那么积极地优化。

如果您有一些代码比您希望的要慢,并且您可以整理出一段完整的代码来演示慢代码,那么也许我们可以建议其他小的更改。但我相信在数据上简单使用constexpri 的模板参数就可以解决问题。


在此示例代码中,我进行了另一项更改。我间接使用tuple_size&lt;array&lt;char, D[i]&gt; &gt; :: value 而不是简单的D_i,这并没有真正改变含义,但我认为它会鼓励旧编译器在编译时进行评估。我的目标是尽可能地匹配代码的原始可读性,例如将整个函数放在一个地方,而不是将其拆分为多个模板。

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

【讨论】:

  • 嗯,实际上在我的程序中,X 值必须在每个循环中更改,因此每次都必须在不同的值上调用该函数,并且使用我的手写模板专业化大约是 4- 5 比使用普通函数(具有常量值)快。我尝试使用 g++ -O2 和 -Os 但没有显着改进。感谢您的第二个回答;)
  • 如果来自DE,并且是DE 数组([]),您是否放入了constexpr?而itemplate&lt;int i&gt; 参数?如果您的 g++ 没有对此进行优化,这很有趣。哪个版本的 g++?
  • 不确定我是用const还是constexpr(抱歉,我找不到测试代码),但我认为它可能没有优化循环,也可能没有优化乘以零,所以我想我没有使用constexpr 来声明数组,而只是使用const
  • 我在答案末尾添加了一些代码。在这种情况下,我进行了另一项更改,以鼓励编译器在编译时进行计算。一个懒惰的(旧的?)编译器可能无法/不愿意完全计算 constexprs,即使它可以这样做。通过分别声明具有D[i]E[i] 成员的array 类型,它必须在编译时进行计算。如果将一些负数放入 D 和 E,您会看到编译器错误,这确认编译器在编译时取得了一些进展。
猜你喜欢
  • 2022-01-19
  • 2012-08-27
  • 1970-01-01
  • 1970-01-01
  • 2023-01-05
  • 2017-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多