あなたの 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と呼びます。
あなたのハードウェアは私のものよりもはるかに重く、メモリも確かに多いようです。私は、Lenovo ThinkPad T420、4 コア i5、8 GB RAM、8 GB スワップ、Linux Mint 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.9 GB まで消費し続け、そのずっと前にすべての CPU が居眠りしていました。
この点は、 MyNthPrime がYourNthPrime
の 2 倍の料金を支払う必要が-ftemplate-depth
あるという事実を考慮すると注目に値します。一般的な知恵では、浪費-ftemplate-depth
はメタプログラムのビルド時間にとって破滅への道であるというものです。しかし、YourNthPrimeとMyNthPrimeのランオフはこれを裏付けるものではなく、まったく逆です。私が得た教訓は、再帰テンプレートをインスタンス化しなければならない深さは、必ずしも量の良い尺度ではないということです実行する必要があるテンプレートのインスタンス化の数であり、メモリ リソースにとって重要なのはその量です。
表面的には似ていませんが、MyNthPrimeとYourNthPrimeはどちらも素数生成のための試行分割アルゴリズムを実装しています。MyNthPrime
は、再帰的なテンプレートのインスタンス化とそれらがむさぼり食うメモリを節約するためにかなり巧妙にコーディングされているという理由だけで、これほど高速です。
YourNthPrimeフィールド 計算用の 4 つの再帰テンプレート。すべて同じ再帰可変個引数リストを使用します。
MyNthPrimeフィールド 2: コンパイラに膨大な数のインスタンス化を半分ほど与えるだけです。
YourNthPrime (私が読んだように) は、手元にある素数の昇順で試行分割を行うことの潜在的な効率を認識しています。手持ちの素数が分割される候補数の 1/2 を超えると、確率は 0 になります。最初に最も可能性の高い約数にヒットし、迅速な評決と終了の見込みを最適化します。
しかし、この効率を利用する際の障害は、手元にある素数が、常に先頭に最大のものを持つ可変個引数テンプレート引数リストによって表されるという事実です。この障害を克服するために、 YourNthPrimeは再帰的な可変個引数テンプレート関数を展開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<>()
YourNthPrimeisPrime<>()
で行われる仕事- 再帰的なトライアル部門 - は、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<>()
1 行でできることを行うには 10 行かかりますがis_prime<>()
、1 行で行うこともできます。
return is_co_prime<N,D0>() && is_co_prime<N,D1,Di...>();
関数の本体である可能性があります。しかし、ここでは効率のために醜いものはきれいなものを打ち負かします。テールに再帰する必要があるたびis_prime<>()
に、そのテールは以前よりも素数が 1 つだけ短くなります。同じことをしなければならないたび
is_co_prime<>()
に、テールは以前より2素数短くなります。その末尾再帰は、最悪の場合、 の末尾再帰よりも浅く、is_prime<>()
半分しかありません。
is_co_prime<>()
最初N
に、使用可能な除数のペアの中で最小かつ最も可能性の高い右辺で候補数を除算し、成功した場合は素数を返しません。それ以外の場合は、そのペアの右側にある除数に再帰し、成功するか使い果たされるまで、1 つ前の除数の試行を続けます。使い果たされた場合にのみ、元のペアの大きい方 (試行した中で最も可能性が低い除数) による試行分割に頼ります。同様に、介在する小さな再帰のそれぞれの中で、最も可能性が低い除数が最後に試行されます。
このアルゴリズムは の小さくて可能性の高い約数にすばやく到達するように見えますがN
、最初にそれらの最小のものに直接到達し、 に従って真の降順確率でそれらを試すことは困難lastArg<>()
です。リストの末尾にある場合、「最小のものに直接到達する」方法は、開始する前にリスト全体を再帰する必要があるメタ関数になるという認識で、このかゆみを鎮めなければなりませんトライアル部門あり。の実装でis_co_prime<>()
は、「一度に 2 つのステップ」でリストを下に移動し、そのように試行分割を行います。
確かに、不運にも最初の最大の除数を「ステップオーバー」し、N
それが底をつき、リストを逆方向に再帰するまで、再び見つけられないことがあります。しかしN
、それが問題になるほど大きくなると、ほとんどの場合、少なくとも 1 つの小さい除数がさらに右にあり、それらすべてを見逃すのは本当に不運です。したがって、途中で最大のものをスキップするリスクは大したことではありません。 ポイントに到達するまで、途中で約数がないことも忘れないでくださいN/2
。つまりN
、途中でいくつかの唯一の除数をスキップすることによる再帰の最悪の場合の無駄は、その時点からリストの末尾に限定されます。
あなたはエラトステネスのふるいのメタプログラムの方がコンパイルが速いと推測していますが、その通りです。素数生成器として、Sieve は Trial Division よりも理論的な複雑さが優れています。2006 年以前のPeter Simonsによるエレガントなテンプレート メタプログラムの実装が
ここにあります。(そして、Peter Wood がコメントしたように、エラトステネスのふるいメタプログラムは、C++ テンプレート システムがチューリング完全であるというニュースを伝えました。)はるかに効率的になったとは思わない。それだけで、Simons Sieve はコンパイル時に 512 番目までのすべての素数を 9 秒未満で生成できます。私のThinkPadで。が必要だ-ftemplate-depth=4000
またはそれを行うには約 0.5GB のメモリしかありません。これは、大きなテンプレートの深さ == 大きなメモリ使用量であるという一般的な知恵に対するさらに顕著な反例です。
そのため、エラトステネスのふるいは、試験部門をほこりの中に置き去りにします。残念ながら、あなたのエクササイズの目的では、ふるいは役に立ちません。整数の上限を入力する必要があり、素数を残して2 と の間の合成数をふるいにかけるため、ふるいと呼ばれます。したがって、正確に N 番目の素数 = を見つけるために適用するには、他の方法ではなく、何らかの方法で計算する必要があります。すでに計算しただけを保持します。これはノーオペレーションです。U
U
Pn
Pn
Pn + 1
Pn + 2
Pn > 2
Pi, 2 <= Pi < Pn
Pn
コメンターの何人かは、コンパイル時のメタプログラミングによって生成できる可能性のある N 番目の素数は、はるかに単純ではるかに高速な手段で事前に知られているか、事前に計算可能であると指摘しています。反対はできませんが、C++11 の機能を使用することで、TMP は実際のユーティリティに向けて大きく前進し、探索する価値があるというあなたの一般的な指摘を支持します。 10年で1秒。
一方、私たちの信じられないほど洗練された C++ コンパイラを離れなくても、このような TMP の問題で、K のクロック速度とメモリを備えた初期のコンピューターを、厳密にモデル化された「言語」でプログラミングする性質をまだ体験できますが、難解な制約の中で! - 古典的な再帰関数理論について。だからこそ、あなたは本当に彼らを愛さなければなりません!