在编译时计算第n个素数

时间:2013-03-08 08:30:20

标签: c++ c++11 template-meta-programming compile-time constexpr

C ++ 11的功能,constexpr和模板参数包,在我看来应该足够强大,可以执行一些相当复杂的计算。我有一个实际应用的一个可能的例子是在编译时计算第n个素数。

我正在寻求实现此计算的方法。如果提出了多个解决方案,那么比较它们可能会很有趣。

为了让您了解我的性能期望:我希望能在合理的桌面硬件上在不到一秒的编译时间内找到第512个素数(即3671)的代码。

4 个答案:

答案 0 :(得分:7)

我实现了最简单的方法,根本不使用模板,它可以工作:

constexpr bool isPrimeLoop(int i, int k) {
    return (k*k > i)?true:(i%k == 0)?false:isPrimeLoop(i, k + 1);
}

constexpr bool isPrime(int i) {
    return isPrimeLoop(i, 2);
}

constexpr int nextPrime(int k) {
    return isPrime(k)?k:nextPrime(k + 1);
}

constexpr int getPrimeLoop(int i, int k) {
// i - nr of primes to advance
// k - some starting prime
    return (i == 0)?k:getPrimeLoop(i - 1, nextPrime(k + 1));
}

constexpr int getPrime(int i) {
    return getPrimeLoop(i, 2);
}

static_assert(getPrime(511) == 3671, "computed incorrectly");

它需要增加constexpr深度,但它很容易适应时间:

$ time g++ -c -std=c++11 vec.cpp -fconstexpr-depth=600

real    0m0.093s
user    0m0.080s
sys 0m0.008s

以下技巧将getPrimeLoop递归的深度减少到对数,因此g ++可以使用默认深度完成(没有可测量的时间惩罚):

constexpr int getPrimeLoop(int i, int k) {
    return (i == 0)?k:
        (i % 2)?getPrimeLoop(i-1, nextPrime(k + 1)):
        getPrimeLoop(i/2, getPrimeLoop(i/2, k));
}

答案 1 :(得分:5)

我怀疑你的1秒目标是任何硬件的触手可及 没有保安人员。不过我相信以下几点 元程序可以关闭它很多:

#include <type_traits>

template<unsigned N>
using boolean = std::integral_constant<bool,N>;

template<unsigned N>
constexpr bool is_co_prime()
{
    return true;
};

template<unsigned N, unsigned D>
constexpr bool is_co_prime()
{
    return N % D != 0;
};

template<unsigned N, unsigned D0, unsigned D1, unsigned ...Di>
constexpr bool is_co_prime()
{
    typedef typename std::conditional<
        is_co_prime<N,D1>(),
        typename std::conditional<
            is_co_prime<N,Di...>(),
            boolean<is_co_prime<N,D0>()>,
            std::false_type
        >::type,
        std::false_type
    >::type type;
    return type::value;
};

template<unsigned N>
constexpr unsigned inc()
{
    return N == 2 ? 3 : N + 2;
}

template<unsigned Counter, unsigned Candidate, unsigned ...Primes>
struct nth_prime;

template<unsigned Candidate, unsigned Prime, unsigned ...Primes>
struct nth_prime<0,Candidate,Prime,Primes...>
{
    static const unsigned value = Prime;
};

template<unsigned Counter, unsigned Candidate = 2, unsigned ...Primes>
struct nth_prime
{
    typedef typename std::conditional<
        is_co_prime<Candidate,Primes...>(),
        nth_prime<Counter - 1,inc<Candidate>(),Candidate,Primes...>,
        nth_prime<Counter,inc<Candidate>(),Primes...>
    >::type type;
    static const unsigned value = type::value;
};

#include <iostream>

using namespace std;

int main()
{
    cout << nth_prime<512>::value << endl;
    return 0;
}

我将此元节目称为 MyNthPrime ,并将您的 YourNthPrime 称为

你似乎拥有比我更强大的硬件,当然还有更多的记忆。一世 拥有常规的联想ThinkPad T420,4核i5,8GB内存,8GB交换,运行Linux 薄荷14,内核3.5.0。你报告它需要3分钟。建立你的 YourNthPrime 。 通过time命令测量,我需要35分钟。构建 YourNthPrime , 没有应用程序运行,但终端和系统监视器。

编译器是GCC 4.7.2,命令行是选项:

-Wall -O0 -std=c++11 -ftemplate-depth=1200

经过的时间分解为:

real    34m2.534s
user    3m40.414s
sys     0m33.450s

构建 MyNthPrime 需要1.5分钟,其中包含:

-Wall -O0 -std=c++11 -ftemplate-depth=2100

并且经过的时间分解为:

real    1m27.832s
user    1m22.205s
sys     0m2.612s

-ftemplate-depth=2100不是转置错字。更多这一点 不久。

MyNthPrime 并不比 YourNthPrime 快23倍。该 细分时间表明 MyNthPrime 实际上是在身边 用户时间的 YourNthPrime 的2.75倍。但他们也表明 YourNthPrime 的构建确实实时丢失了农场。它以前如何 做其无效的9/10事件?当然,交换。

这两个版本都在45秒内嘲笑了我的8GB系统RAM的95%,但 MyNthPrime 在那附近突然出现并没有交换。 YourNthPrime 继续吃掉掉 空间达到峰值3.9GB,很久以前我的所有CPU都在打瞌睡。

当您使用 MyNthPrime 这一事实时,这一点值得注意 需要比 YourNthPrime 多两倍-ftemplate-depth。 民间智慧是一条奢侈的-ftemplate-depth通往道路 破坏了元程序的构建时间,因为它等于奢侈 内存消耗,只需要滑入大量交换和 你看油漆干了。但 YourNthPrime MyNthPrime 的决定确实如此 不要忍受这一点 - 恰恰相反。我接受的教训是 您必须实例化递归模板的 depth 不是 始终可以很好地衡量实例化您的模板的数量 必须这样做,这是对你的记忆资源很重要的数量。

虽然它们看起来并不相似,但 MyNthPrime YourNthPrime , 两者都实现了用于素数生成的试分算法。 MyNthPrime 只是因为它更精确地编码为更快 保留递归模板实例,以及它们吞噬的内存。

YourNthPrime 字段用于计算的4个递归模板, all 使用相同的递归可变参数模板参数列表 MyNthPrime 字段2:它只是给编译器大约一半 许多巨大的实例要做。

YourNthPrime (正如我所读到的)感知到潜在的效率 按照手中的素数递增顺序进行试验 - 因为它的机会 成功的划分增加到较小的素数;曾经是一个素数 在手中超过被分割的候选人数的1/2,机会为0。 首先击中最可能的除数,然后优化你的快速前景 判决和退出。但是利用这种效率的障碍是这样的事实 手头的素数由具有最大的可变参数模板参数列表表示 永远在脑海中。要克服这个障碍 YourNthPrime 部署递归 variadic模板函数lastArg<>(),有效地扭转了 在这些部门中使用素数的顺序。

lastArg<>()向模板展示了素数 功能:

template<unsigned i, unsigned head, unsigned... tail>
constexpr bool isPrime() {
    return i % head && isPrime<i, tail...>();
}

按递增顺序对下一个候选人i进行递归试验划分 由素数head, tail...。它在这里我认为你期待lastArg<>() 通过确保head始终是他的下一个最佳前景来付出代价 让你走出&&的昂贵的右手边。

但要实现这个lastArg<>()本身递归遍历整个 在您获得机会之前,每次调用时手头的素数列表 对i的判决。让isPrime<>()变得更便宜 尽可能地遍历手头的素数,随时测试i, 免除lastArg<>()并保存所有递归实例 物。

isPrime<>() YourNthPrime 中完成的工作 - 递归试验部门 - 通过以下方式在 MyNthPrime 中完成:

template<unsigned N, unsigned D0, unsigned D1, unsigned ...Di>
constexpr bool is_co_prime()
{
    typedef typename std::conditional<
        is_co_prime<N,D1>(),
        typename std::conditional<
            is_co_prime<N,Di...>(),
            boolean<is_co_prime<N,D0>()>,
            std::false_type
        >::type,
        std::false_type
    >::type type;
    return type::value;
};

is_co_prime<>()需要10行才能完成is_prime<>()所做的事情, 我本可以在一行中完成它:

return is_co_prime<N,D0>() && is_co_prime<N,D1,Di...>();

可能是该功能的主体。但丑陋的祸害是美丽的 效率在这里。每次is_prime<>()必须进入尾巴, 那条尾巴只比以前短了一个。每次 is_co_prime<>()必须做同样的事情,尾部是两个素数 比以前更短。它的尾部递归较浅 最糟糕的情况比is_prime<>()最差,只有一半。

is_co_prime<>()用右手划分候选人编号N - 最小和最有可能 - 任何一对可用的除数第一个, 并且在成功时返回no-prime;否则仍然可以向任何除数递归 在那一对的右边,继续下一个一个的试验分裂 直到成功或疲惫。只有在筋疲力尽时才会诉诸于此 由原来对中较大的试验师 - 最少 可能是它试过的任何除数。同样在每个内部 中间较小的递归,最不可能的除数得到 最后尝试过。

虽然可以看到这个算法可以快速地获得更小的和 N的可能性除数,it会坚持直截了当 首先是最小的,并以真正的下降可能性来尝试它们, 根据{{​​1}}。我们必须通过认可来平息这种痒 任何方式&#34;直接到最小的&#34;,当它在 列表的尾部,将是一个必须自己递归的元函数 在我们开始试用分部之前的整个清单。 lastArg<>()的实施将我们排在了列表之下 &#34;一次两步&#34;在这样做的情况下进行试验。

是的,有时它会不幸地踩到&#34; is_co_prime<>()的最大除数 第一次,然后再次找到它,直到除非它达到最低点 并向后递归列表。但是一旦N大到可以做到这一点 事情上,至少会有至少一个较小的除数 在右边,错过所有这些都是非常不吉利的。所以 在下降途中跳过最大的风险并不是什么大不了的事。记得 在我们前进的路上,也没有任何除数 达到N点。这意味着最坏情况下的递归浪费 在向下的路上跳过某些N/2的唯一除数是有限的 从那一点开始到列表的尾部。

你推测Erathosthenes元程序的Sieve可能 编译得更快,你是对的。作为主要发电机,Sieve更好 理论复杂性比试验科。优雅的模板 Peter Simons的元计划实施,是 here,可追溯到2006年或之前。 (和 正如彼得·伍德评论的那样,一种非终止的Erathosthenes筛子 元程序打破了C ++模板系统图灵完备的消息。) 使用C ++ 11设施Simons&#39;元程序可以缩写很多但是 我不认为提高效率。就这样,西蒙斯 Sieve可以在编译时生成所有素数,直到第512个 不到9秒。在我的ThinkPad上。它需要N或 要做到这一点,但只有大约0.5GB的内存 - 甚至更多 逮捕反对大模板深度==的民间智慧的反例 大内存使用率。

所以Erathosthenes&#39; Sieve离开审判部在尘土中挣扎。 可悲的是,为了锻炼,Sieve是没用的。它被称为 sieve 因为我们必须输入一个整数上限-ftemplate-depth=4000 从2和U之间的复合数中筛选,留下素数。 因此,应用它来准确找到第N个素数= U而不是 可能还有其他任何一种,你必须以其他方式计算Pn, 给Sieve一个上限Pn(或Pn + 1Pn + 2),然后 抛弃它返回给你的所有Pn > 2,然后继续 只是你已经计算过的Pi, 2 <= Pi < Pn。这是一个无操作。

一些评论者指出任何Nth prime的身份 你可能会通过编译时生成元编程 预先知道或事先通过更简单和更广泛的计算 更快意味着。我不能不同意,但我支持你的一般观点 在C ++ 11设施中,TMP向实际效用迈出了巨大的一步 这是非常值得探索的 - 随着时间的推移更是如此 现在编译将在十年内完成。

同时,甚至没有离开我们不可思议的复杂C ++编译器, 对于像这样的TMP问题,我们仍然可以体验编程的本质 一台早期的计算机,具有K的时钟速度和记忆,采用&#34;语言&#34; 紧紧模仿 - 但在神秘的约束下! - 古典递归 功能理论。这就是为什么你真的要爱他们!

答案 2 :(得分:1)

我自己尝试了这个,并编写了以下实现:

template<unsigned... args> constexpr unsigned countArgs();
template<> constexpr unsigned countArgs() { return 0; }
template<unsigned head, unsigned... tail>
constexpr unsigned countArgs() { return 1 + countArgs<tail...>(); }

template<unsigned last>
constexpr unsigned lastArg() { return last; }
template<unsigned head, unsigned next, unsigned... tail>
constexpr unsigned lastArg() { return lastArg<next, tail...>(); }

template<unsigned i> constexpr bool isPrime() { return true; }
template<unsigned i, unsigned head, unsigned... tail>
constexpr bool isPrime()
{ return i % head && isPrime<i, tail...>(); }

template<bool found, unsigned i, unsigned... primesSoFar> struct nextPrime
{ static constexpr unsigned val =
    nextPrime<isPrime<i + 2, primesSoFar...>(), i + 2, primesSoFar...>::val; };
template<unsigned i, unsigned... primesSoFar> struct
nextPrime<true, i, primesSoFar...> { static constexpr unsigned val = i; };

template<unsigned n, unsigned... primesSoFar> struct nthPrimeImpl
{ static constexpr unsigned val = nthPrimeImpl<n - 1, primesSoFar...,
    nextPrime<false, lastArg<primesSoFar...>(), primesSoFar...>::val>::val; };
template<unsigned... primesSoFar> struct nthPrimeImpl<0, primesSoFar...>
{ static constexpr unsigned val = lastArg<primesSoFar...>(); };

template<unsigned n>
constexpr unsigned nthPrime() {
  return n == 1 ? 2 : nthPrimeImpl<n - 2, 3>::val;
}

constexpr unsigned p512 = nthPrime<512>();
static_assert(p512 == 3671, "computed incorrectly");

这需要将gcc的最大模板深度增加到超过默认值900(在我的gcc 4.7.2中),例如通过-ftemplate-depth=1200。它太慢了:我的硬件需要大约3 分钟。所以我非常希望在不同的答案中提供更有效的代码。

就计算方法而言,上面的内容与trial division类似。 sieve of Eratosthenes可能表现得更好,但到目前为止,我无法想出以兼容兼容的方式编写它的方法。

答案 3 :(得分:1)

answer by Mike Kinghan让我思考着我以前没有的线条。如果模板实例化是导致如此严重的内存消耗的问题,那么我们如何减少这种情况呢?我最终提出了一个方案,其中代替了迄今为止发现的所有素数的参数包,我使用了一系列类型,每个类型都引用了之前的类型,以及这些类型中的一系列静态函数,它们可以使用类型中的类型之前。

我将在下面粘贴的结果仍然比the one suggested by zch慢很多,但我觉得分享它很有趣,因为它可能是其他应用程序的有用方法。

template<unsigned N> struct NthPrime {
  typedef NthPrime<N - 1> previous;
  static constexpr unsigned prime = previous::nextPrime();
  static constexpr unsigned nextPrime() { return nextCoprime(prime + 2); }
  static constexpr unsigned nextCoprime(unsigned x) {
    // x is a candidate. We recurse to obtain a number which is
    // coprime to all smaller primes, then check that value against
    // the current prime.
    return checkCoprime(previous::nextCoprime(x));
  }
  static constexpr unsigned checkCoprime(unsigned x) {
    // if x is coprime to the current prime as well, then it is the
    // next prime. Otherwise we have to try the next candidate.
    return (x % prime) ? x : nextCoprime(x + 2);
  }
};

template<> struct NthPrime<1> {
  static constexpr unsigned prime = 2;
  static constexpr unsigned nextPrime() {
    return 3;
  }
  static constexpr unsigned nextCoprime(unsigned x) {
    return x; // x is guaranteed to be odd, so no need to check anything.
  }
};

template<unsigned n>
constexpr unsigned nthPrime() {
  return NthPrime<n>::prime;
}

constexpr unsigned p512 = nthPrime<512>();
static_assert(p512 == 3671, "computed incorrectly");

上面的野兽需要修改constexpr深度和模板深度。以下值是我的编译器的明确界限。

time g++-4.7.2 -c -fconstexpr-depth=519 -ftemplate-depth=2042 -std=c++11 foo.cc

real    0m0.397s
user    0m0.368s
sys     0m0.025s