C ++ 11的功能,constexpr
和模板参数包,在我看来应该足够强大,可以执行一些相当复杂的计算。我有一个实际应用的一个可能的例子是在编译时计算第n个素数。
我正在寻求实现此计算的方法。如果提出了多个解决方案,那么比较它们可能会很有趣。
为了让您了解我的性能期望:我希望能在合理的桌面硬件上在不到一秒的编译时间内找到第512个素数(即3671)的代码。
答案 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 + 1
,Pn + 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