【问题标题】:C++ calculate and sort vector at compile timeC++ 在编译时计算和排序向量
【发布时间】:2015-12-16 02:31:15
【问题描述】:

我有一个class A,它有一个std::vector<int> 作为属性。 A 需要在创建A 的实例时填充此向量。 计算可能需要一些时间,我想知道:

  1. 可以在编译时完成。
  2. 向量也可以在编译时排序

我不熟悉元编程,而且我暂时没有找到方法。这不是特定于操作系统的问题。

这是A.cpp 文件:

#include "A.h"
#define SIZEV 100

A::A()
{
    fillVector();
}

void A::fillVector()
{
    // m_vector is an attribute of class "A"
    // EXPECTATION 1 : fill the vector with the following calculation at compile time

    const int a=5;
    const int b=7;
    const int c=9;

    for(int i=0;i<SIZEV;i++){
        for(int j=0;j<SIZEV;j++){
            for(int k=0;k<SIZEV;k++){
                this->m_vector.push_back(a*i+b*j+c*k);
            }
        }
    }

    // EXPECTATION 2 : sort the vector as compile time 
}

std::vector<int> A::getVector() const
{
    return m_vector;
}

void A::setVector(const std::vector<int> &vector)
{
    m_vector = vector;
}

还有main.cpp(Qt 应用但没关系):

#include <QCoreApplication>
#include "A.h"

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

    A a;
    auto vec = a.getVector();

    // use vec, etc ...

    return app.exec();
}

【问题讨论】:

  • 你的意思是在运行时而不是在“编译”时?
  • 我的意思是在编译时。
  • 你考虑过代码生成吗?
  • 您确定计算花费的时间最多吗?对我来说,它更像是调用 push_back。您是否尝试过使用初始大小定义 m_vector,然后在循环中设置值?甚至使用数组?
  • @BlackPopa:谁说过“复制粘贴”?

标签: c++ c++11 metaprogramming


【解决方案1】:

您可以实现skew heap 在编译时对整数进行排序。以下示例适用于 c++17。

#include <type_traits>
#include <utility>

template <class T, T... s>
using iseq = std::integer_sequence<T, s...>;

template <class T, T v>
using ic = std::integral_constant<T, v>;

template <class T, T v1, T v2>
constexpr auto ic_less_impl(ic<T, v1>, ic<T, v2>) -> ic<bool, v1 < v2>;
template <class ic1, class ic2>
using ic_less = decltype(ic_less_impl(ic1(), ic2()));

template <bool b>
using bool_cond_t = std::conditional_t<b, std::true_type, std::false_type>;

struct nil {};

template <class T, T v, T... s>
constexpr auto iseq_front_impl(iseq<T, v, s...>) -> ic<T, v>;
template <class T>
constexpr auto iseq_front_impl(iseq<T>) -> nil;
template <class seq>
using iseq_front = decltype(iseq_front_impl(seq()));

template <class T, T v, T... s>
constexpr auto iseq_pop_front_impl(iseq<T, v, s...>) -> iseq<T, s...>;
template <class seq>
using iseq_pop_front = decltype(iseq_pop_front_impl(seq()));

template <class T, T v, T... s>
constexpr auto iseq_append_impl(iseq<T, s...>, ic<T, v>) -> iseq<T, s..., v>;
template <class T, T v>
constexpr auto iseq_append_impl(nil, ic<T, v>) -> iseq<T, v>;
template <class seq, class c>
using iseq_append = decltype(iseq_append_impl(seq(), c()));

template <class seq>
using iseq_is_empty = bool_cond_t<std::is_same<iseq_front<seq>, nil>::value>;

template <class X, class L, class R>
struct skew_heap {};

template <class X, class L, class R>
constexpr auto skh_get_top_impl(skew_heap<X, L, R>) -> X;
template <class H>
using skh_get_top = decltype(skh_get_top_impl(H()));

template <class X, class L, class R>
constexpr auto skh_get_left_impl(skew_heap<X, L, R>) -> L;
template <class H>
using skh_get_left = decltype(skh_get_left_impl(H()));

template <class X, class L, class R>
constexpr auto skh_get_right_impl(skew_heap<X, L, R>) -> R;
template <class H>
using skh_get_right = decltype(skh_get_right_impl(H()));

template <class H>
using skh_is_empty = bool_cond_t<std::is_same<H, nil>::value>;

template <class H1, class H2>
constexpr auto skh_merge_impl(H1, H2) -> decltype(auto) {
    if constexpr (skh_is_empty<H1>::value) {
        return H2{};
    } else if constexpr (skh_is_empty<H2>::value) {
        return H1{};
    } else {
        using x1 = skh_get_top<H1>;
        using l1 = skh_get_left<H1>;
        using r1 = skh_get_right<H1>;

        using x2 = skh_get_top<H2>;
        using l2 = skh_get_left<H2>;
        using r2 = skh_get_right<H2>;

        if constexpr (ic_less<x2, x1>::value) {
            using new_r2 = decltype(skh_merge_impl(H1(), r2()));
            return skew_heap<x2, new_r2, l2> {};
        } else {
            using new_r1 = decltype(skh_merge_impl(r1(), H2()));
            return skew_heap<x1, new_r1, l1>{};
        }
    }
}
template <class H1, class H2>
using skh_merge = decltype(skh_merge_impl(H1(), H2()));

template <class H1, class IC1>
using skh_push = skh_merge<H1, skew_heap<IC1, nil, nil>>;

template <class H>
using skh_pop = skh_merge<skh_get_left<H>, skh_get_right<H>>;

template <class H, class seq>
constexpr auto skh_heapify_impl(H, seq) -> decltype(auto) {
    if constexpr (iseq_is_empty<seq>::value) {
        return H{};
    } else {
        using val = iseq_front<seq>;
        return skh_heapify_impl(skh_push<H, val>{}, iseq_pop_front<seq>{});
    }
}
template <class seq>
using skh_heapify = decltype(skh_heapify_impl(nil(), seq()));

template <class H, class seq>
constexpr auto skh_to_sortseq_impl(H, seq) -> decltype(auto) {
    if constexpr (skh_is_empty<H>::value) {
        return seq{};
    } else {
        using val = skh_get_top<H>;
        return skh_to_sortseq_impl(skh_pop<H>{}, iseq_append<seq, val>{});
    }
}
template <class H>
using skh_to_sortseq = decltype(skh_to_sortseq_impl(H(), nil()));

template <class seq>
using sort_seq = skh_to_sortseq<skh_heapify<seq>>;

static_assert(std::is_same<iseq<int, 1, 2, 3, 4, 5, 6, 7, 8, 9>, sort_seq<iseq<int, 2, 3, 5, 8, 9, 6, 7, 1, 4>>>::value);

【讨论】:

    【解决方案2】:

    这是一个简单的整数编译时排序。它对每个元素起作用,计算出它在列表中的位置。由此得出每个位置应该是什么。然后它建立一个插入到适当位置的新列表。它在复杂性方面可能不如以前的解决方案那么有效(它是 O(n^2)),但它更容易理解并且它不使用递归。

    #include <initializer_list>
    #include <array>
    #include <tuple>
    
    template<int... members>
    struct IntList
    {
        constexpr bool operator==(IntList) const { return true; }
    
        template<int... others>
        constexpr bool operator==(IntList<others...>) const { return false; }
    
        template<int idx>
        static constexpr auto at() 
        {
            return std::get<idx>(std::make_tuple(members...));
        }
    
        template<int x>
        static constexpr auto indexOf()
        {
            int sum {};
            auto _ = { 0, (x > members ? ++sum : 0)... };
            return sum;
        }
    
        template<int x>
        static constexpr auto count()
        {
            int sum {};
            auto _ = { 0, (x == members ? ++sum : 0)... };
            return sum;
        }
    
        template<int i>
        static constexpr auto ith()
        {
            int result{};
            auto _ = {
                ( i >= indexOf<members>() && i < indexOf<members>() + count<members>() ? 
                  result = members : 0 )...
            };
            return result;
        }
    
        template<std::size_t... i>
        static constexpr auto sortImpl(std::index_sequence<i...>)
        {
            return IntList< ith<i>()... >();
        }
    
        static constexpr auto sort() 
        {
            return sortImpl(std::make_index_sequence<sizeof...(members)>());
        }
    };
    
    static_assert(IntList<1, 2, 3>().at<1>() == 2, "");
    
    static_assert(IntList<>().indexOf<1>()           == 0, "");
    static_assert(IntList<1>().indexOf<1>()          == 0, "");
    static_assert(IntList<1, 2, 3, 4>().indexOf<3>() == 2, "");
    
    static_assert(IntList<>().count<1>()        == 0, "");
    static_assert(IntList<1>().count<1>()       == 1, "");
    static_assert(IntList<1, 1>().count<1>()    == 2, "");
    static_assert(IntList<2, 2, 1>().count<1>() == 1, "");
    static_assert(IntList<1, 2, 1>().count<1>() == 2, "");
    
    static_assert(IntList<>().sort()        == IntList<>(),        "");
    static_assert(IntList<1>().sort()       == IntList<1>(),       "");
    static_assert(IntList<1, 2>().sort()    == IntList<1, 2>(),    "");
    static_assert(IntList<2, 1>().sort()    == IntList<1, 2>(),    "");
    static_assert(IntList<3, 2, 1>().sort() == IntList<1, 2, 3>(), "");
    static_assert(IntList<2, 2, 1>().sort() == IntList<1, 2, 2>(), "");
    static_assert(IntList<4, 7, 2, 5, 1>().sort() == IntList<1, 2, 4, 5, 7>(), "");
    static_assert(IntList<4, 7, 7, 5, 1, 1>().sort() == IntList<1, 1, 4, 5, 7, 7>(), "");
    

    【讨论】:

    • 这是漂亮的代码。所有代码都应该很漂亮。
    • 谢谢你,太客气了。
    【解决方案3】:

    数据是从0SIZEV * (a+b+c)的整数,但整数个数是SIZEV3。它是一组范围较小的密集整数,因此 CountingSort 是完美的(您永远不需要构建未排序的数组,只需在生成时递增计数)。

    不管计数/前缀总和如何,CountingSort 绝对会在启动时间对向量进行排序,而不是其他排序,保持其他一切相同。

    您可以将数据的紧凑形式(O(cuberoot(n)) 大小)保留为 prefix sums 的向量,以便在 O(log (cuberoot(n))) 时间内从 m_vector 查找(二进制搜索前缀总和),其中 n 是 m_vector 的长度。见下文。

    根据缓存/内存延迟,实际上不扩展 m_vector 可能会或可能不会赢得性能。如果需要一定范围的值,您可以非常快速地从前缀和中动态生成 m_vector 的顺序元素。

    class A {
        // vector<uint16_t> m_counts;  // needs to be 32b for SIZEV>=794 (found experimentally).
    
        vector<uint32_t> m_pos;     // values are huge: indices into m_vector, up to SIZEV**3 - 1
        vector<uint16_t> m_vector;  // can be 16b until SIZEV>3121: max val is only (a+b+c) * (SIZEV-1)
    }
    void A::fillVector()
    {
        const int a=5;
        const int b=7;
        const int c=9;
    
        const auto max_val = (SIZEV-1) * (a+b+c);
    
        m_vector.reserve(SIZEV*SIZEV*SIZEV);
        m_vector.resize(0);
        // or clear it, but that writes tons of mem, unless you use a custom Allocator::construct to leave it uninit
        // http://en.cppreference.com/w/cpp/container/vector/resize
    
        m_pos.resize(max_val + 1);  // again, ideally avoid zeroing
                      // but if not, do it before m_counts
    
        m_counts.clear();  // do this one last, so it's hot in cache even if others wasted time writing zeros.
        m_counts.resize(max_val + 1); // vector is now zeroed
        // Optimization: don't have a separate m_counts.
        // zero and count into m_pos, then do prefix summing in-place
    
    
        // manually strength-reduce the multiplication to addition
        // in case the compiler decides it won't, or can't prove it won't overflow the same way
        // Not necessary with gcc or clang: they both do this already
        for(int kc=c*(SIZEV-1) ; kc >= 0 ; kc-=c) {
          for(int jb=b*(SIZEV-1) ; jb >= 0 ; jb-=b) {
            for(int ia=a*(SIZEV-1) ; ia >= 0 ; ia-=a) {
              m_counts[kc + jb + ia]++;
              // do the smallest stride in the inner-most loop, for better cache locality
            }
          }
        }
    // write the early elements last, so they'll be hot in the cache when we're done
    
    
        int val = 0;
        uint32_t sum = 0;
        for ( auto &count : m_counts ) {
           m_vector.insert(m_vector.end(), count, val++);
           // count is allowed to be zero for vector::insert(pos, count, value)
           m_pos[val] = sum;   // build our vector of prefix sums
           sum += count;
    
           //count = (sum+=count);  // in-place conversion to prefix sums
        }
        assert(m_vector.size() == SIZEV*SIZEV*SIZEV);
    }
    

    或者,不是实际扩展一个 1.6GB 的数组,而是计算 Prefix sums 的计数,为您提供该索引运行的起始位置的向量作为 m_vector 中的一个元素。即idx = m_pos[val]; m_vector[idx] == val。 (这在 val m_count 中有零,并在 m_pos 中重复)

    无论如何,您可以将读取的m_vector[i] 替换为在m_pos 中对i 进行二分搜索。您正在寻找 m_pos 中值 m_vector[i] 找到该索引。 (或类似的东西;我可能有一个错误。)

    哈希表不起作用,因为您需要将多个 i 值映射到从 0..(750*(a+b+c)) 开始的每个数字。 (所有is,其中m_vector[i] 具有相同的值。)

    如果您需要一系列顺序元素,请将它们动态生成到 tmp 缓冲区中。查看m_pos[i+1] 以了解下一个具有不同值的元素何时到来。 (查看m_counts 可能会节省一些减法,但您最好只使用m_pos 中的差异来反转前缀和,以避免缓存未命中/缓存污染接触第二个数组。)

    实际上,m_counts 可能根本不需要作为类成员保留,只是 FillVector 中的一个临时成员。或者 FillVector 可以计入m_pos,并将其就地转换为前缀和。

    理想情况下,您可以使用模板做一些聪明的事情,为 m_counts 和 m_vector 选择足够宽但不超过所需的类型。 IDK数论,所以我不知道如何证明不会有一个桶m_counts溢出uint16_t平均计数将是 750**3 / (750*(5+7+9)) = 26786,它们肯定聚集在 m_counts 的高端。在实践中,SIZEV=793 可以使用 uint16_t 计数器,而 SIZEV=794 produces several counts > 65536(感谢 Chris 提供的工作示例,我可以轻松地对其进行测试)。

    m_vector 可以是 uint16_t 直到 (SIZEV-1)*(a+b+c) &gt; MAX_UINT16 (65535)。即直到 SIZEV >= 3122,此时m_vector 占用 28.3 GiB 的 RAM。


    在 SIZEV = 750 时,m_pos 大约是 L1 缓存大小的 2 倍(Intel CPU)(750*(5+7+9) * 4B per short = 63000B)。如果编译器做得很好并使用条件移动而不是不可预测的分支指令进行二进制搜索,这可能会非常快。它肯定会为您节省大量主内存流量,如果您有多个线程,这很有价值。

    或者,永远不要接触m_vector 意味着您可以处理需要比您拥有更多内存的问题大小来存储列表。

    如果您想在首先创建 m_counts(使用三重嵌套循环)时通过优化缓存获得真正的创意,请让最内层的循环向前然后向后,而不是两次都朝同一个方向。这仅对非常大的 SIZEV 或其他超线程对缓存施加很大压力时才有意义。

      for(int kc=c*(SIZEV-1) ; kc >= 0 ; kc-=c) {
        for(int jb=b*(SIZEV-1) ; jb >= 0 ; jb-=b) {
    
          for(int ia=0 ; ia<SIZEV*a ; ia+=a)
            counts[kc + jb + ia]++;
          if (! (jb-=b )) break;
          for(int ia=a*(SIZEV-1) ; ia >= 0 ; ia-=a)
            counts[kc + jb + ia]++;
    
        }
      }
    

    倒数到零(有或没有双向内循环)很可能是下一个循环开始的一个小胜利,然后当计数变高时,它会变得受内存限制,做大的 memset。扫描前锋以进行前缀总和也是一个胜利。


    我之前的回答,大概是死路一条:

    有没有希望为排序向量中的ith 元素找到一个封闭形式的公式?或者甚至是动态生成它的 O(log i) 算法?

    除非您在访问该向量时需要大量顺序元素,否则动态计算它可能会更快。内存很慢,CPU 很快,所以如果你能在大约 150 个时钟周期内计算 a[i],你就领先了。 (假设每次访问都是缓存未命中,或者不触及所有向量内存会减少程序其余部分的缓存未命中)。

    如果我们能做到这一点,理论上我们可以首先按顺序编写排序数组。

    要做到这一点:将常量打乱为a &lt;= b &lt;= c

    0, a, [a*2 .. a*int(b/a)], b, [b + a .. b + a*int((c-b)/a) mixed with b*2 .. b*int(c/b)], c, [some number of b*x + a*y], c+a, [more b*x + a*y], ...

    好的,所以这变成了一个组合混乱,这个想法可能不可行。至少,不适用于任何 a、b 和 c 的一般情况。

    当 a=5、b=7、c=9:

    0, 5=a, 7=b, 9=c, 10=2a, 12=b+a, 14=2b, 14=c+a, 15=3a, 16=c+b, 18=2c

    我太困了,看不到模式,但这里有一个更长的列表

    # bash
    limit=5; for ((i=0 ; i<limit ; i++)); do
                 for ((j=0 ; j<limit ; j++)); do 
                   for ((k=0 ; k<limit ; k++)); do 
                     printf "%2d: %d %d %d\n" $((5*i + 7*j + 9*k)) $i $j $k; 
               done; done; done | sort -n | cat -n
         1   0: 0 0 0
         2   5: 1 0 0
         3   7: 0 1 0
         4   9: 0 0 1
         5  10: 2 0 0
         6  12: 1 1 0
         7  14: 0 2 0
         8  14: 1 0 1
         9  15: 3 0 0
        10  16: 0 1 1
        11  17: 2 1 0
        12  18: 0 0 2
        13  19: 1 2 0
        14  19: 2 0 1
        15  20: 4 0 0
        16  21: 0 3 0
        17  21: 1 1 1
        18  22: 3 1 0
        19  23: 0 2 1
        20  23: 1 0 2
        21  24: 2 2 0
        22  24: 3 0 1
        23  25: 0 1 2
        24  26: 1 3 0
        25  26: 2 1 1
        26  27: 0 0 3
        27  27: 4 1 0
        28  28: 0 4 0
        29  28: 1 2 1
        30  28: 2 0 2
        31  29: 3 2 0
        32  29: 4 0 1
        33  30: 0 3 1
        34  30: 1 1 2
        35  31: 2 3 0
        36  31: 3 1 1
        37  32: 0 2 2
        38  32: 1 0 3
        39  33: 1 4 0
        40  33: 2 2 1
        41  33: 3 0 2
        42  34: 0 1 3
        43  34: 4 2 0
        44  35: 1 3 1
        45  35: 2 1 2
        46  36: 0 0 4
        47  36: 3 3 0
        48  36: 4 1 1
        49  37: 0 4 1
        50  37: 1 2 2
        51  37: 2 0 3
        52  38: 2 4 0
        53  38: 3 2 1
        54  38: 4 0 2
        55  39: 0 3 2
        56  39: 1 1 3
        57  40: 2 3 1
        58  40: 3 1 2
        59  41: 0 2 3
        60  41: 1 0 4
        61  41: 4 3 0
        62  42: 1 4 1
        63  42: 2 2 2
        64  42: 3 0 3
        65  43: 0 1 4
        66  43: 3 4 0
        67  43: 4 2 1
        68  44: 1 3 2
        69  44: 2 1 3
        70  45: 3 3 1
        71  45: 4 1 2
        72  46: 0 4 2
        73  46: 1 2 3
        74  46: 2 0 4
        75  47: 2 4 1
        76  47: 3 2 2
        77  47: 4 0 3
        78  48: 0 3 3
        79  48: 1 1 4
        80  48: 4 4 0
        81  49: 2 3 2
        82  49: 3 1 3
        83  50: 0 2 4
        84  50: 4 3 1
        85  51: 1 4 2
        86  51: 2 2 3
        87  51: 3 0 4
        88  52: 3 4 1
        89  52: 4 2 2
        90  53: 1 3 3
        91  53: 2 1 4
        92  54: 3 3 2
        93  54: 4 1 3
        94  55: 0 4 3
        95  55: 1 2 4
        96  56: 2 4 2
        97  56: 3 2 3
        98  56: 4 0 4
        99  57: 0 3 4
       100  57: 4 4 1
       101  58: 2 3 3
       102  58: 3 1 4
       103  59: 4 3 2
       104  60: 1 4 3
       105  60: 2 2 4
       106  61: 3 4 2
       107  61: 4 2 3
       108  62: 1 3 4
       109  63: 3 3 3
       110  63: 4 1 4
       111  64: 0 4 4
       112  65: 2 4 3
       113  65: 3 2 4
       114  66: 4 4 2
       115  67: 2 3 4
       116  68: 4 3 3
       117  69: 1 4 4
       118  70: 3 4 3
       119  70: 4 2 4
       120  72: 3 3 4
       121  74: 2 4 4
       122  75: 4 4 3
       123  77: 4 3 4
       124  79: 3 4 4
       125  84: 4 4 4
    

    【讨论】:

    • 我没有收到你的新答案的通知,我会仔细阅读,这个方法看起来很有趣。 :)
    • @BlackPopa 我正在使用计数排序准备答案,但彼得打败了我。我可以确认它要快得多。如果您有兴趣here is my benchmark.
    • @Chris Drew:我看过了。非常感谢,确实差别很大!
    • @ChrisDrew:感谢代码链接。使用工作代码检查 uint16_t 何时溢出很方便。我在答案中链接到该代码的修改版本。我们在计算所需数组的最大值和大小时都出现了一个错误。我想我现在说对了。我注意到你没有反转循环,在内部循环中做更小的步幅以获得更好的缓存局部性。另外,自从我最初的回答之后,我有了另一个想法:做三重循环倒数到零,所以当你去计算前缀和时,counts 的前面在缓存中很热(或将其扩展为m_vector。)
    【解决方案4】:

    虽然可以这样做 (live example),但您不应该这样做。这将花费大量的编译时间。

    编译器不是为快速、高效的数字海量处理而设计的。现在,请将您的编译时工作限制在相对简单的事情上,而不是对 1000 万个元素进行排序。

    即使您编写“合规”代码,今天的大多数编译器也会对您产生爆炸性影响。我写的代码很早就死了,尽管我试图小心我的递归深度限制。

    无论如何,为了后代:

    template<class T>struct tag{using type=T;};
    template<class Tag>using type_t=typename Tag::type;
    
    template<int...Xs> struct values { constexpr values() {}; };
    
    template<int...Xs> constexpr values<Xs...> values_v = {};
    
    template<class...Vs> struct append;
    template<class...Vs> using append_t=type_t<append<Vs...>>;
    template<class...Vs> constexpr append_t<Vs...> append_v = {};
    
    template<> struct append<>:tag<values<>>{};
    template<int...Xs>struct append<values<Xs...>>:tag<values<Xs...>>{};
    template<int...Lhs, int...Rhs, class...Vs>
    struct append<values<Lhs...>,values<Rhs...>,Vs...>:
        tag<append_t<values<Lhs...,Rhs...>,Vs...>>
    {};
    
    template<int...Lhs>
    constexpr values<Lhs...> simple_merge( values<Lhs...>, values<> ) { return {}; }
    template<int...Rhs>
    constexpr values<Rhs...> simple_merge( values<>, values<Rhs...> ) { return {}; }
    constexpr values<> simple_merge( values<>, values<> ) { return {}; }
    
    template<int L0, int...Lhs, int R0, int...Rhs>
    constexpr auto simple_merge( values<L0, Lhs...>, values<R0, Rhs...> )
    -> std::conditional_t<
        (R0 < L0),
        append_t< values<R0>, decltype( simple_merge( values<L0,Lhs...>{}, values<Rhs...>{} ) ) >,
        append_t< values<L0>, decltype( simple_merge( values<Lhs...>{}, values<R0, Rhs...>{} ) ) >
    > {
        return {};
    }
    
    template<class Lhs, class Rhs>
    using simple_merge_t = decltype( simple_merge( Lhs{}, Rhs{} ) );
    template<class Lhs, class Rhs>
    constexpr simple_merge_t<Lhs, Rhs> simple_merge_v = {};
    
    template<class Values, size_t I> struct split
    {
    private:
        using one = split<Values, I/2>;
        using two = split<typename one::rhs, I-I/2>;
    public:
        using lhs = append_t< typename one::lhs, typename two::lhs >;
        using rhs = typename two::rhs;
    };
    template<class Values, size_t I> using split_t=type_t<split<Values, I>>;
    
    template<class Values> struct split<Values, 0>{
        using lhs = values<>;
        using rhs = Values;
    };
    template<int X0, int...Xs> struct split<values<X0, Xs...>, 1> {
        using lhs = values<X0>;
        using rhs = values<Xs...>;
    };
    template<class Values, size_t I> using before_t = typename split<Values, I>::lhs;
    template<class Values, size_t I> using after_t = typename split<Values, I>::rhs;
    
    template<size_t I>using index_t=std::integral_constant<size_t, I>;
    template<int I>using int_t=std::integral_constant<int, I>;
    template<int I>constexpr int_t<I> int_v={};
    
    template<class Values> struct front;
    template<int X0, int...Xs> struct front<values<X0, Xs...>>:tag<int_t<X0>>{};
    template<class Values> using front_t=type_t<front<Values>>;
    template<class Values> constexpr front_t<Values> front_v = {};
    
    template<class Values, size_t I>
    struct get:tag<front_t< after_t<Values, I> >> {};
    template<class Values, size_t I> using get_t = type_t<get<Values, I>>;
    template<class Values, size_t I> constexpr get_t<Values, I> get_v = {};
    
    template<class Values>
    struct length;
    template<int...Xs>
    struct length<values<Xs...>>:tag<index_t<sizeof...(Xs)>> {};
    template<class Values> using length_t = type_t<length<Values>>;
    template<class Values> constexpr length_t<Values> length_v = {};
    
    template<class Values> using front_half_t = before_t< Values, length_v<Values>/2 >;
    template<class Values> constexpr front_half_t<Values> front_half_v = {};
    template<class Values> using back_half_t = after_t< Values, length_v<Values>/2 >;
    template<class Values> constexpr back_half_t<Values> back_half_v = {};
    
    template<class Lhs, class Rhs>
    struct least : tag< std::conditional_t< (Lhs{}<Rhs{}), Lhs, Rhs > > {};
    template<class Lhs, class Rhs> using least_t = type_t<least<Lhs, Rhs>>;
    template<class Lhs, class Rhs>
    struct most : tag< std::conditional_t< (Lhs{}>Rhs{}), Lhs, Rhs > > {};
    template<class Lhs, class Rhs> using most_t = type_t<most<Lhs, Rhs>>;
    
    template<class Values>
    struct pivot {
    private:
        using a = get_t<Values, 0>;
        using b = get_t<Values, length_v<Values>/2>;
        using c = get_t<Values, length_v<Values>-1>;
        using d = most_t< least_t<a,b>, most_t< least_t<b,c>, least_t<a,c> > >;
    public:
        using type = d;
    };
    template<int X0, int X1>
    struct pivot<values<X0, X1>>: tag< most_t< int_t<X0>, int_t<X1> > > {};
    
    template<class Values> using pivot_t = type_t<pivot<Values>>;
    template<class Values> constexpr pivot_t<Values> pivot_v = {};
    
    template<int P>
    constexpr values<> lower_split( int_t<P>, values<> ) { return {}; }
    template<int P, int X0>
    constexpr std::conditional_t< (X0<P), values<X0>, values<> > lower_split( int_t<P>, values<X0> ) { return {}; }
    
    template<int P, int X0, int X1, int...Xs >
    constexpr auto lower_split( int_t<P>, values<X0, X1, Xs...> )
    -> append_t<
        decltype(lower_split( int_v<P>, front_half_v<values<X0, X1, Xs...>> )),
        decltype(lower_split( int_v<P>, back_half_v<values<X0, X1, Xs...>> ))
    >{ return {}; }
    template<int P>
    constexpr values<> upper_split( int_t<P>, values<> ) { return {}; }
    template<int P, int X0>
    constexpr std::conditional_t< (X0>P), values<X0>, values<> > upper_split( int_t<P>, values<X0> ) { return {}; }
    template<int P, int X0, int X1, int...Xs>
    constexpr auto upper_split( int_t<P>, values<X0, X1, Xs...> )
    -> append_t<
        decltype(upper_split( int_v<P>, front_half_v<values<X0, X1, Xs...>> )),
        decltype(upper_split( int_v<P>, back_half_v<values<X0, X1, Xs...>> ))
    >{ return {}; }
    
    template<int P>
    constexpr values<> middle_split( int_t<P>, values<> ) { return {}; }
    template<int P, int X0>
    constexpr std::conditional_t< (X0==P), values<X0>, values<> > middle_split( int_t<P>, values<X0> ) { return {}; }
    template<int P, int X0, int X1, int...Xs>
    constexpr auto middle_split( int_t<P>, values<X0, X1, Xs...> )
    -> append_t<
        decltype(middle_split( int_v<P>, front_half_v<values<X0, X1, Xs...>> )),
        decltype(middle_split( int_v<P>, back_half_v<values<X0, X1, Xs...>> ))
    >{ return {}; }
    
    template<class Values>
    using lower_split_t = decltype(lower_split( pivot_v<Values>, Values{} ) );
    template<class Values> constexpr lower_split_t<Values> lower_split_v = {};
    template<class Values>
    using upper_split_t = decltype(upper_split( pivot_v<Values>, Values{} ) );
    template<class Values> constexpr upper_split_t<Values> upper_split_v = {};
    template<class Values>
    using middle_split_t = decltype(middle_split( pivot_v<Values>, Values{} ) );
    template<class Values> constexpr middle_split_t<Values> middle_split_v = {};
    
    constexpr values<> simple_merge_sort( values<> ) { return {}; }
    template<int X>
    constexpr values<X> simple_merge_sort( values<X> ) { return {}; }
    
    template<class Values>
    using simple_merge_sort_t = decltype( simple_merge_sort( Values{} ) );
    template<class Values>
    constexpr simple_merge_sort_t<Values> simple_merge_sort_v = {};
    
    template<int X0, int X1, int...Xs>
    constexpr auto simple_merge_sort( values<X0, X1, Xs...> )
    -> 
    simple_merge_t<
        simple_merge_t<
            simple_merge_sort_t<lower_split_t<values<X0, X1, Xs...>>>, simple_merge_sort_t<upper_split_t<values<X0, X1, Xs...>>>
        >,
        middle_split_t<values<X0, X1, Xs...>>
    >
    { return {}; }
    
    
    template<class Values>constexpr Values cross_add( Values ) { return {}; }
    template<class Values>constexpr values<> cross_add( values<>, Values ) { return {}; }
    template<int A0, int...B>constexpr values<(B+A0)...> cross_add( values<A0>, values<B...> ) { return {}; }
    
    template<int A0, int A1, int...A, int...B>
    constexpr auto cross_add( values<A0, A1, A...>, values<B...>)
    -> append_t<
        decltype(cross_add( front_half_v<values<A0, A1, A...>>, values_v<B...> ) ),
        decltype(cross_add( back_half_v<values<A0, A1, A...>>, values_v<B...> ) )
    > { return {}; }
    
    template<class V0, class V1, class V2, class... Vs>
    constexpr auto cross_add( V0, V1, V2, Vs... )
    -> decltype(
        cross_add( cross_add( V0{}, V1{} ), V2{}, Vs{}... )
    ) { return {}; }
    
    template<class...Vs>
    using cross_add_t = decltype( cross_add(Vs{}...) );
    template<class...Vs>
    constexpr cross_add_t<Vs...> cross_add_v = {};
    
    template<int X, int...Xs>
    constexpr values<(X*Xs)...> scale( int_t<X>, values<Xs...> ) { return {}; }
    template<class X, class Xs>
    using scale_t = decltype( scale(X{}, Xs{}) );
    template<class X, class Xs> constexpr scale_t<X,Xs> scale_v = {};
    
    template<int X0, int...Xs> struct generate_values : generate_values<X0-1, X0-1, Xs...> {};
    template<int...Xs> struct generate_values<0,Xs...>:tag<values<Xs...>>{};
    template<int X0> using generate_values_t = type_t<generate_values<X0>>;
    

    三个编译时间的中位数合并排序和叉积生成器。通过努力,我可以大大减少行数。

    使用constexpr std::array 可能会比上述纯类型解决方案更快。

    【讨论】:

    • 我对模板元编程没有太多经验。模板使用en.wikipedia.org/wiki/Counting_sort 会更好吗?请参阅我的运行时 CountingSort 答案(对于像这样的密集高度重复数据,它应该比 std::sort 快 方式)。
    【解决方案5】:

    std::vector&lt;int&gt; 没有任何constexpr 构造函数(因为constexpr 不允许动态内存分配)。所以你不能在编译时对std::vector&lt;int&gt; 进行排序。

    您可以在编译时为常量N 创建一个std::array&lt;int, N&gt;,但您必须编写自己的排序例程,因为std::sort 也不是constexpr

    您还可以编写一个Boost.MPL 编译时向量或列表并使用其中的sort 例程。但这不会像 std::array 那样扩展。

    另一个攻击角度可能是将向量存储到static 变量中,并在程序初始化时进行排序。您的程序启动时间会稍长一些,但不会影响其余的主要功能。

    由于排序是O(N log N),您甚至可以进行两步构建并将排序后的向量写入文件,然后将其编译/链接到您的主程序,或者在程序启动时将其加载到O(N)static 变量。

    【讨论】:

    • 静态变量看起来是个好主意,我想我会去的。这也很容易实现:)
    • @BlackPopa 即使是静态的,您也可以通过两步构建进行优化,但这有点麻烦
    • 两步构建,太棒了!
    【解决方案6】:

    也许不完全是您要查找的内容,但您可以编写一个单独的程序来计算向量,对其进行排序,然后将其输出到一个列表中。然后你就可以读入那个文件了。

    如果从磁盘读取速度太慢,您还可以将输出转换为合法的 C++ 文件,该文件初始化您的类的实例,该实例拥有满足您要求的硬编码值。然后可以将其链接回您的主项目并进行编译,本质上提供与您在此处概述的更复杂的元编程任务相同的功能。

    【讨论】:

    • 即使您将预先计算的数组嵌入到可执行文件中,它仍然必须从磁盘读取,因此该选择不会影响性能考虑。
    • 这是真的,我想。这与通过模板元编程将数组嵌入可执行文件有什么不同吗?
    • @AGML:不,一点也不。您的方式还有一个巨大的好处,即只需要在需要的数据更改时重建海量目标文件,而不是每次类中的更改都会使编译器在编译时对 1.6GB 的数据进行排序。然而,普遍的共识(我同意)是让应用程序在运行时执行此操作会更好地提高应用程序的性能,即使不考虑分发 1.6GB 可执行文件的问题。 CountingSort 很快。
    【解决方案7】:

    可以预先计算的冗长计算的经典方法是计算结果作为构建过程的一部分,生成一个对结果进行硬编码的.cpp(在具有嵌入式资源的平台上也可以使用这些)。 .

    但是,这里的计算非常简单,慢的部分可能只是分配,如果你想将数据保存在std::vector 中,必须在运行时发生。如果您可以使用 C 样式的数组,则可以如上所述将其全部放入可执行文件中,但这会产生 4 MB 大的可执行文件,并且从磁盘加载它导致的减速将抵消预先计算的任何速度优势。

    IOW:当计算成本高且输出量小时,在构建时进行预计算是有意义的。你的情况与频谱完全相反,所以我会避免它。

    【讨论】:

    • @BlackPopa:更糟糕的是,这会使 1.6 GB 的可执行文件!
    • @BlackPopa 是您在问题中的计算,您正在做的实际计算?如果您预先reserve() 所需的空间量(SIZEV*SIZEV*SIZEV),您可能会发现它会更快。
    • @BlackPopa:一般来说,请记住,如果您的代码具有良好的渐近复杂度 (O(n), O(n log n)) 并且受内存限制(而不是受 CPU 限制) ) 通常从磁盘读取无法击败从头开始的计算,因为它是 O(n) 且具有更大的常数(RAM 的传输速率大约比磁盘好两个数量级)。
    • @BlackPopa 我不知道你做了什么,但reserve() 不是push_back() 的替代品——你想在push_back() 的循环之前调用m_vector.reserve(SIZEV*SIZEV*SIZEV)。没有其他变化,你还是push_back()。如果不是您测量错误,那会更快。
    • 在我使用std::vector 标量类型的所有测试中,使用resize 而不是reserve 更快,然后使用operator[] 存储元素,作为零初始化元素非常便宜,您可以避免push_back 完成的所有大小/容量检查。
    猜你喜欢
    • 1970-01-01
    • 2011-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-09
    • 2015-05-29
    相关资源
    最近更新 更多