16

考慮すべきことは、64 ビットの符号なし整数をその素因数に (比較的迅速に) 因数分解するために使用できる次の関数です。因数分解は確率論的ではない (つまり、正確である) ことに注意してください。このアルゴリズムは、最新のハードウェアでは、数秒で数が素数であるか、非常に大きな因数がほとんどないことを検出するのに十分な速さです。

質問: できれば確率論的アプローチ (例: Miller-Rabin) を使用せずに、非常に大きな符号なし 64 ビット整数を (任意の) より高速に因数分解できるように、提示されたアルゴリズムをシングル スレッドに保ちながら、何らかの改善を行うことはできますか?素数を決定するため?

// system specific typedef for ulong should go here (or use boost::uint64_t)
typedef unsigned __int64 ulong;
typedef std::vector<ulong> ULongVector;

// Caller needs to pass in an empty factors vector
void GetFactors(ULongVector &factors, ulong num)
{
  // Num has to be at least 2 to contain "prime" factors
  if (num<2)
    return;

  ulong workingNum=num;
  ulong nextOffset=2; // Will be used to skip multiples of 3, later

  // Factor out factors of 2
  while (workingNum%2==0)
  {
    factors.push_back(2);
    workingNum/=2;
  }

  // Factor out factors of 3
  while (workingNum%3==0)
  {
    factors.push_back(3);
    workingNum/=3;
  }

  // If all of the factors were 2s and 3s, done...
  if (workingNum==1)
    return;

  // sqrtNum is the (inclusive) upper bound of our search for factors
  ulong sqrtNum=(ulong) sqrt(double(workingNum+0.5));

  // Factor out potential factors that are greate than or equal to 5
  // The variable n represents the next potential factor to be tested
  for (ulong n=5;n<=sqrtNum;)
  {
    // Is n a factor of the current working number?
    if (workingNum%n==0)
    {
      // n is a factor, so add it to the list of factors
      factors.push_back(n);

      // Divide current working number by n, to get remaining number to factor
      workingNum/=n;

      // Check if we've found all factors
      if (workingNum==1)
        return;

      // Recalculate the new upper bound for remaining factors
      sqrtNum=(ulong) sqrt(double(workingNum+0.5));

      // Recheck if n is a factor of the new working number, 
      // in case workingNum contains multiple factors of n
      continue;
    }

    // n is not or is no longer a factor, try the next odd number 
    // that is not a multiple of 3
    n+=nextOffset;
    // Adjust nextOffset to be an offset from n to the next non-multiple of 3
    nextOffset=(nextOffset==2UL ? 4UL : 2UL);
  }

  // Current workingNum is prime, add it as a factor
  factors.push_back(workingNum);
}

ありがとう

編集:さらにコメントを追加しました。ベクトルが参照によって渡される理由は、呼び出し間でベクトルを再利用できるようにし、動的な割り当てを回避するためです。関数内でベクトルが空にならない理由は、現在の「num」の係数をベクトル内に既にある係数に追加するという奇妙な要件を考慮に入れるためです。

関数自体はきれいではなく、リファクタリングできますが、問題はアルゴリズムを高速化する方法です。したがって、関数をより見やすく、読みやすく、または C++ 風にする方法についての提案はありません。それは子供の遊びです。このアルゴリズムを改善して、(証明された) 因子をより速く見つけられるようにすることは、難しい部分です。

更新: Potatoswatterはこれまでにいくつかの優れたソリューションを提供しています。下部にある彼の MMX ソリューションも必ずチェックしてください。

4

7 に答える 7

19

このようなアプローチを(事前に生成された)ふるいと比較してください。Modulo はコストがかかるため、どちらのアプローチも基本的に 2 つのことを行います: 潜在因子の生成と modulo 演算の実行です。どちらのプログラムも、モジュロにかかるサイクルよりも少ないサイクルで新しい候補因子を合理的に生成する必要があるため、どちらのプログラムもモジュロに拘束されます。

与えられたアプローチは、すべての整数の一定の割合、つまり 2 と 3 の倍数、つまり 75% を除外します。(指定された) 4 分の 1 の数値がモジュロ演算子の引数として使用されます。これをスキップ フィルターと呼びます。

一方、ふるいはモジュロ演算子への引数として素数のみを使用し、連続する素数間の平均差は素数定理によって 1/ln(N) になるように管理されます。たとえば、e ^20 は 5 億未満なので、5 億を超える数が素数である可能性は 5% 未満です。2^32 までのすべての数値を考慮すると、経験則として 5% が適切です。

divしたがって、ふるいは、スキップ フィルターの5 分の 1 の時間で操作に費やすことができます。次に考慮すべき要素は、ふるいが素数を生成する速度、つまりメモリまたはディスクから素数を読み取る速度です。1 つの素数を取得するのに 4div秒よりも速い場合は、ふるいの方が高速です。私の表によるとdiv、Core2 のスループットは最大で 12 サイクルあたり 1 です。これらは難しい除算の問題になるので、控えめに素数あたり 50 サイクルの予算を立てましょう。2.5 GHz プロセッサの場合、これは 20 ナノ秒です。

20 ns で、50 MB/秒のハード ドライブは約 1 バイトを読み取ることができます。簡単な解決策は、素数ごとに 4 バイトを使用することです。これにより、ドライブが遅くなります。しかし、私たちはもっと賢くなることができます。すべての素数を順番にエンコードしたい場合は、それらの違いをエンコードするだけです。ここでも、予想される差は 1/ln(N) です。また、それらはすべて偶数であるため、さらに少し節約できます。また、それらはゼロになることはありません。これにより、マルチバイト エンコーディングへの拡張が自由になります。そのため、素数ごとに 1 バイトを使用すると、最大 512 の差を 1 バイトに格納でき、そのウィキペディアの記事によると、最大 303371455241 になります。

したがって、ハード ドライブによっては、格納された素数のリストの素数性の検証速度がほぼ同じになるはずです。RAM に格納できる場合 (203 MB であるため、後続の実行でおそらくディスク キャッシュにヒットする可能性があります)、FSB の速度は通常、FSB の幅 (バイト単位) よりも小さい係数でプロセッサの速度と異なるため、問題は完全に解消されます。 — つまり、FSB はサイクルごとに複数の素数を転送できます。次に、改善の要因は、分割操作の 5 倍の削減です。これは、以下の実験結果によって裏付けられています。

もちろん、マルチスレッドもあります。素数またはスキップ フィルター処理された候補の範囲を異なるスレッドに割り当てることができるため、どちらのアプローチも途方もなく並列になります。どういうわけかモジュロを排除しない限り、並列除算器回路の数を増やすことを伴わない最適化はありません。

ここにそのようなプログラムがあります。テンプレート化されているため、bignum を追加できます。

/*
 *  multibyte_sieve.cpp
 *  Generate a table of primes, and use it to factorize numbers.
 *
 *  Created by David Krauss on 10/12/10.
 *
 */

#include <cmath>
#include <bitset>
#include <limits>
#include <memory>
#include <fstream>
#include <sstream>
#include <iostream>
#include <iterator>
#include <stdint.h>
using namespace std;

char const primes_filename[] = "primes";
enum { encoding_base = (1<< numeric_limits< unsigned char >::digits) - 2 };

template< typename It >
unsigned decode_gap( It &stream ) {
    unsigned gap = static_cast< unsigned char >( * stream ++ );

    if ( gap ) return 2 * gap; // only this path is tested

    gap = ( decode_gap( stream )/2-1 ) * encoding_base; // deep recursion
    return gap + decode_gap( stream ); // shallow recursion
}

template< typename It >
void encode_gap( It &stream, uint32_t gap ) {
    unsigned len = 0, bytes[4];

    gap /= 2;
    do {
        bytes[ len ++ ] = gap % encoding_base;
        gap /= encoding_base;
    } while ( gap );

    while ( -- len ) { // loop not tested
        * stream ++ = 0;
        * stream ++ = bytes[ len + 1 ];
    }
    * stream ++ = bytes[ 0 ];
}

template< size_t lim >
void generate_primes() {
    auto_ptr< bitset< lim / 2 > > sieve_p( new bitset< lim / 2 > );
    bitset< lim / 2 > &sieve = * sieve_p;

    ofstream out_f( primes_filename, ios::out | ios::binary );
    ostreambuf_iterator< char > out( out_f );

    size_t count = 0;

    size_t last = sqrtl( lim ) / 2 + 1, prev = 0, x = 1;
    for ( ; x != last; ++ x ) {
        if ( sieve[ x ] ) continue;
        size_t n = x * 2 + 1; // translate index to number
        for ( size_t m = x + n; m < lim/2; m += n ) sieve[ m ] = true;
        encode_gap( out, ( x - prev ) * 2 );
        prev = x;
    }

    for ( ; x != lim / 2; ++ x ) {
        if ( sieve[ x ] ) continue;
        encode_gap( out, ( x - prev ) * 2 );
        prev = x;
    }

    cout << prev * 2 + 1 << endl;
}

template< typename I >
void factorize( I n ) {
    ifstream in_f( primes_filename, ios::in | ios::binary );
    if ( ! in_f ) {
        cerr << "Could not open primes file.\n"
                "Please generate it with 'g' command.\n";
        return;
    }

    while ( n % 2 == 0 ) {
        n /= 2;
        cout << "2 ";
    }
    unsigned long factor = 1;

    for ( istreambuf_iterator< char > in( in_f ), in_end; in != in_end; ) {
        factor += decode_gap( in );

        while ( n % factor == 0 ) {
            n /= factor;
            cout << factor << " ";
        }

        if ( n == 1 ) goto finish;
    }

    cout << n;
finish:
    cout << endl;
}

int main( int argc, char *argv[] ) {
    if ( argc != 2 ) goto print_help;

    unsigned long n;

    if ( argv[1][0] == 'g' ) {
        generate_primes< (1ul<< 32) >();
    } else if ( ( istringstream( argv[1] ) >> n ).rdstate() == ios::eofbit )
        factorize( n );
    } else goto print_help;

    return 0;

print_help:
    cerr << "Usage:\n\t" << argv[0] << " <number> -- factorize number.\n"
            "\t" << argv[0] << " g -- generate primes file in current directory.\n";
}

2.2 GHz MacBook Pro でのパフォーマンス:

dkrauss$ time ./multibyte_sieve g
4294967291

real    2m8.845s
user    1m15.177s
sys    0m2.446s
dkrauss$ time ./multibyte_sieve 18446743721522234449
4294967231 4294967279 

real    0m5.405s
user    0m4.773s
sys 0m0.458s
dkrauss$ time ./mike 18446743721522234449
4294967231 4294967279
real    0m25.147s
user    0m24.170s
sys 0m0.096s
于 2010-10-13T20:27:41.687 に答える
9

私の他の答えはかなり長く、これとはかなり異なるので、ここに別のものがあります.

最初の 2 つの素数の倍数を単に除外したり、関連するすべての素数をそれぞれ 1 バイトにエンコードしたりするのではなく、このプログラムは、8 ビット (具体的には 2 から 211) に収まるすべての素数の倍数を除外します。したがって、33% を渡す代わりに、数値の場合、これは約 10% を除算演算子に渡します。

素数は 3 つの SSE レジスタに保持され、ランニング カウンターを使用したモジュラスは別の 3 つのレジスタに保持されます。カウンターの素数のモジュラスがゼロに等しい場合、カウンターは素数にはなりません。また、いずれかの法が 1 に等しい場合、(counter+2) は (counter+30) まで素数にすることはできません。偶数は無視され、+3、+6、+5 などのオフセットはスキップされます。ベクトル処理では、16 バイト サイズの変数を一度に更新できます。

マイクロ最適化を完全に実行した後 (ただし、インライン ディレクティブ以外にプラットフォーム固有のものは何もありません)、パフォーマンスが 1.78 倍向上しました (ラップトップで 13.4 秒に対して 24 秒)。bignum ライブラリ (非常に高速なライブラリであっても) を使用すると、利点が大きくなります。読みやすい最適化前のバージョンについては、以下を参照してください。

/*
 *  factorize_sse.cpp
 *  Filter out multiples of the first 47 primes while factorizing a number.
 *
 *  Created by David Krauss on 10/14/10.
 *
 */

#include <cmath>
#include <sstream>
#include <iostream>
#include <xmmintrin.h>
using namespace std;

inline void remove_factor( unsigned long &n, unsigned long factor ) __attribute__((always_inline));
void remove_factor( unsigned long &n, unsigned long factor ) {
    while ( n % factor == 0 ) {
        n /= factor;
        cout << factor << " ";
    }
}

int main( int argc, char *argv[] ) {
    unsigned long n;

    if ( argc != 2
        || ( istringstream( argv[1] ) >> n >> ws ).rdstate() != ios::eofbit ) {
        cerr << "Usage: " << argv[0] << " <number>\n";
        return 1;
    }

    int primes[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
                     53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
                     131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211 };
    for ( int *p = primes; p < primes + sizeof primes/sizeof *primes; ++ p ) {
        remove_factor( n, * p );
    }

    //int histo[8] = {}, total = 0;

    enum { bias = 15 - 128 };
    __m128i const prime1 =       _mm_set_epi8( 21, 21, 21, 22, 22, 26, 26, 17, 19, 23, 29, 31, 37, 41, 43, 47 ),
            prime2 =             _mm_set_epi8( 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127 ),
            prime3 =             _mm_set_epi8( 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211 ),
            vbias = _mm_set1_epi8( bias ),
            v3 = _mm_set1_epi8( 3+bias ), v5 = _mm_set1_epi8( 5+bias ), v6 = _mm_set1_epi8( 6+bias ), v8 = _mm_set1_epi8( 8+bias ),
            v9 = _mm_set1_epi8( 9+bias ), v11 = _mm_set1_epi8( 11+bias ), v14 = _mm_set1_epi8( 14+bias ), v15 = _mm_set1_epi8( 15+bias );
    __m128i mod1 = _mm_add_epi8( _mm_set_epi8(  3, 10, 17,  5, 16,  6, 19,  8,  9, 11, 14, 15, 18, 20, 21, 23 ), vbias ),
            mod2 = _mm_add_epi8( _mm_set_epi8( 26, 29, 30, 33, 35, 36, 39, 41, 44, 48,  50,  51,  53,  54,  56,  63 ), vbias ),
            mod3 = _mm_add_epi8( _mm_set_epi8(  65,  68,  69,  74,  75,  78,  81,  83,  86,  89,  90,  95,  96,  98,  99, 105 ), vbias );

    for ( unsigned long factor = 1, limit = sqrtl( n ); factor <= limit + 30; factor += 30 ) {
        if ( n == 1 ) goto done;

        // up to 2^32, distribution of number candidates produced (0 up to 7) is
        // 0.010841     0.0785208   0.222928    0.31905     0.246109    0.101023    0.0200728   0.00145546 
        unsigned candidates[8], *cand_pen = candidates;
        * cand_pen = 6;
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1,  v3 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2,  v3 ), _mm_cmpeq_epi8( mod3,  v3 ) ) ) );
        * cand_pen = 10;                                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1,  v5 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2,  v5 ), _mm_cmpeq_epi8( mod3,  v5 ) ) ) );
        * cand_pen = 12;                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1,  v6 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2,  v6 ), _mm_cmpeq_epi8( mod3,  v6 ) ) ) );
        * cand_pen = 16;                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1,  v8 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2,  v8 ), _mm_cmpeq_epi8( mod3,  v8 ) ) ) );
        * cand_pen = 18;                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1,  v9 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2,  v9 ), _mm_cmpeq_epi8( mod3,  v9 ) ) ) );
        * cand_pen = 22;                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1, v11 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2, v11 ), _mm_cmpeq_epi8( mod3, v11 ) ) ) );
        * cand_pen = 28;                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1, v14 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2, v14 ), _mm_cmpeq_epi8( mod3, v14 ) ) ) );
        * cand_pen = 30;                                                                                                                            
        cand_pen += !( _mm_movemask_epi8( _mm_cmpeq_epi8( mod1, v15 ) ) | _mm_movemask_epi8( _mm_or_si128( _mm_cmpeq_epi8( mod2, v15 ), _mm_cmpeq_epi8( mod3, v15 ) ) ) );

        /*++ total;
        ++ histo[ cand_pen - candidates ];

        cout << "( ";
        while ( cand_pen != candidates ) cout << factor + * -- cand_pen << " ";
        cout << ")" << endl; */

        mod1 = _mm_sub_epi8( mod1, _mm_set1_epi8( 15 ) ); // update residuals
        __m128i mask1 = _mm_cmplt_epi8( mod1, _mm_set1_epi8( 1+bias ) );
        mask1 = _mm_and_si128( mask1, prime1 ); // residual goes to zero or negative?
        mod1 = _mm_add_epi8( mask1, mod1 ); // combine reset into zero or negative

        mod2 = _mm_sub_epi8( mod2, _mm_set1_epi8( 15 ) );
        __m128i mask2 = _mm_cmplt_epi8( mod2, _mm_set1_epi8( 1+bias ) );
        mask2 = _mm_and_si128( mask2, prime2 );
        mod2 = _mm_add_epi8( mask2, mod2 );

        mod3 = _mm_sub_epi8( mod3, _mm_set1_epi8( 15 ) );
        __m128i mask3 = _mm_cmplt_epi8( mod3, _mm_set1_epi8( 1+bias ) );
        mask3 = _mm_and_si128( mask3, prime3 );
        mod3 = _mm_add_epi8( mask3, mod3 );

        if ( cand_pen - candidates == 0 ) continue;
        remove_factor( n, factor + candidates[ 0 ] );
        if ( cand_pen - candidates == 1 ) continue;
        remove_factor( n, factor + candidates[ 1 ] );
        if ( cand_pen - candidates == 2 ) continue;
        remove_factor( n, factor + candidates[ 2 ] );
        if ( cand_pen - candidates == 3 ) continue;
        remove_factor( n, factor + candidates[ 3 ] );
        if ( cand_pen - candidates == 4 ) continue;
        remove_factor( n, factor + candidates[ 4 ] );
        if ( cand_pen - candidates == 5 ) continue;
        remove_factor( n, factor + candidates[ 5 ] );
        if ( cand_pen - candidates == 6 ) continue;
        remove_factor( n, factor + candidates[ 6 ] );
    }

    cout << n;
done:
    /*cout << endl;
    for ( int hx = 0; hx < 8; ++ hx ) cout << (float) histo[hx] / total << " ";*/
    cout << endl;
}

.

dkrauss$ /usr/local/bin/g++ main.cpp -o factorize_sse -O3 --profile-use
dkrauss$ time ./factorize_sse 18446743721522234449
4294967231 4294967279 

real    0m13.437s
user    0m13.393s
sys 0m0.011s

以下は、上記の最初のドラフトです。含まれる最適化

  • サイクリック カウンター マージを無条件にします (分岐を回避します)。
  • ループを 15 倍に展開し、ストライドを 30 に増やして ILP を取得します。
    • あなたの最適化に触発されました。
    • 30 は、2、3、および 5 の倍数を無料で削除するので、スイート スポットのようです。
    • 5 から 15 の間の素数は、1 つのストライドで複数の倍数を持つ可能性があるため、ベクター内のさまざまな位相に複数のコピーを配置します。
  • 因数分解しremove_factorます。
  • 条件付きで予測不可能なremove_factor呼び出しを非分岐配列書き込みに変更します。
  • 呼び出しで最終ループを完全に展開remove_factorし、関数が常にインライン化されていることを確認します。
    • 候補の中には常に 7 の倍数が存在するため、展開された最後の繰り返しを削除します。
  • 十分に小さい残りのすべての素数を含む別のベクトルを追加します。
  • カウンターにバイアスを追加してスペースを増やし、さらに別のベクトルを追加します。これで、最大 16 ビットに達することなくフィルタリングできる可能性のある素数はあと 6 つしかなく、レジスタも不足しています。ループには、3 つの素数ベクトル、3 つのモジュラス ベクトル、検索する 8 つの定数、およびそれぞれ 1 つの定数が必要です。インクリメントして範囲チェックを行います。すると16になります。
    • このアプリケーションではゲインは最小限 (ただし正) ですが、この手法の本来の目的は、他の回答のふるいの素数をフィルター処理することでした。どうぞお楽しみに…</li>

読み取り可能なバージョン:

/*
 *  factorize_sse.cpp
 *  Filter out multiples of the first 17 primes while factorizing a number.
 *
 *  Created by David Krauss on 10/14/10.
 *
 */

#include <cmath>
#include <sstream>
#include <iostream>
#include <xmmintrin.h>
using namespace std;

int main( int argc, char *argv[] ) {
    unsigned long n;

    if ( argc != 2
        || ( istringstream( argv[1] ) >> n >> ws ).rdstate() != ios::eofbit ) {
        cerr << "Usage: " << argv[0] << " <number>\n";
        return 1;
    }

    int primes[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59 };
    for ( int *p = primes; p < primes + sizeof primes/sizeof *primes; ++ p ) {
        while ( n % * p == 0 ) {
            n /= * p;
            cout << * p << " ";
        }
    }

    if ( n != 1 ) {
        __m128i       mod   = _mm_set_epi8( 1, 2, 3,  5,  6,  8,  9, 11, 14, 15, 18, 20, 21, 23, 26, 29 );
        __m128i const prime = _mm_set_epi8( 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59 ),
                      one = _mm_set1_epi8( 1 );

        for ( unsigned long factor = 1, limit = sqrtl( n ); factor < limit; ) {
            factor += 2;
            __m128i mask = _mm_cmpeq_epi8( mod, one ); // residual going to zero?
            mod = _mm_sub_epi8( mod, one ); // update other residuals
            if ( _mm_movemask_epi8( mask ) ) {
                mask = _mm_and_si128( mask, prime ); // reset cycle if going to zero
                mod = _mm_or_si128( mask, mod ); // combine reset into zeroed position

            } else while ( n % factor == 0 ) {
                n /= factor;
                cout << factor << " ";
                if ( n == 1 ) goto done;
            }
        }
        cout << n;
    }
done:
    cout << endl;
}
于 2010-10-15T11:53:12.243 に答える
5

フェルマーの因数分解法は、大きくなりすぎて遅くなる前に停止する限り、大きな素因数のペアを簡単かつ迅速に見つけることができます。ただし、乱数に関する私のテストでは、そのようなケースは非常にまれであり、改善が見られませんでした.

...素数性を決定するための確率論的アプローチ (例: Miller-Rabin) を使用しない

一様分布では、入力の 75% で 10 億回のループ反復が必要になるため、決定的でない答えが得られて試行分割に戻らなければならない場合でも、まず決定論的ではない手法に 100 万回の操作を費やす価値があります。

Pollard の Rho Method の Brent 版は非常に優れていることがわかりましたが、コーディングと理解はより複雑です。私が見た最良の例は、このフォーラムのディスカッションです。この方法は運に依存しますが、多くの場合、十分な価値があります。

Miller-Rabin 素数性テストは、実際には約 10^15 まで決定論的であり、実りのない検索の手間を省くことができます。

私は数十のバリエーションを試し、int64 値を因数分解するために次のように決めました。

  1. 小さな因数での試行分割。(最初の 8000 個の事前計算された素数を使用します。)
  2. Pollard の Rho を 10 回試行、それぞれ 16 回の反復を使用
  3. sqrt(n) への試行分割。

Pollard の Rho は必ずしも素数ではない因数を検出するため、再帰を使用してそれらを因数分解できることに注意してください。

于 2010-10-16T17:24:43.090 に答える
2

このコードはかなり遅いです、そして私は私が理由を理解しているとかなり確信しています。信じられないほど遅くはありませんが、10〜20%の範囲では間違いなく遅くなります。分割は、ループスルーごとに1回実行する必要はありませんが、それを実行する唯一の方法は、呼び出しsqrtまたは同様のものを実行することです。

// system specific typedef for ulong should go here (or use boost::uint64_t)
typedef std::vector<ulong> ULongVector;

void GetFactors(ULongVector &factors, ulong num)
{
  if (num<2)
    return;

  ulong workingNum=num;
  ulong nextOffset=2;

  while (workingNum%2==0)
  {
    factors.push_back(2);
    workingNum/=2;
  }

  while (workingNum%3==0)
  {
    factors.push_back(3);
    workingNum/=3;
  }

  ulong n = 5;
  while ((workingNum != 1) && ((workingNum / n) >= n)) {
    // Is workingNum divisible by n?
    if (workingNum%n==0)
    {
      // n is a factor!
      // so is the number multiplied by n to get workingNum

      // Insert n into the list of factors
      factors.push_back(n);

      // Divide working number by n
      workingNum/=n;
    } else {
      n+=nextOffset;
      nextOffset=(nextOffset==2UL ? 4UL : 2UL);
    }
  }

  if (workingNum != 1) {
    // workingNum is prime, add it to the list of factors        
    factors.push_back(workingNum);
  }
}
于 2010-10-12T21:20:00.327 に答える
2

これらがどれほど効果的かはわかりませんが、代わりに

while (workingNum%2==0)

あなたができる

while (workingNum & 1 == 0)

gcc や msvc (または使用しているコンパイラ) が workingNum%2 式を変更するのに十分賢いかどうかはわかりませんが、除算を行って edx でモジュラスを調べている可能性があります...

私の次の提案は、コンパイラによっては完全に不要になる可能性がありますが、メソッド呼び出しの前に workingNum /= 3 を配置してみてください。G++ は、不必要な除算を認識し、商を eax で使用するのに十分賢いかもしれません (これは、より大きなループ内でも行うことができます)。または、より徹底的な (しかし面倒な) アプローチは、次のコードをインライン アセンブリすることです。

while (workingNum%3==0)
{
  factors.push_back(3);
  workingNum/=3;
}

コンパイラはおそらく、モジュラー操作を除算に変換してから、edx でモジュラスを調べています。問題は、除算を再度実行していることです (そして、ループの条件で暗黙的に除算を実行したことをコンパイラーが認識しているとは思えません)。したがって、これをインラインでアセンブルできます。これには 2 つの問題があります。

1) push_back(3) のメソッド呼び出し。これはレジスタを台無しにする可能性があり、これは完全に不要になります。

2) workingNum のレジスターを取得しますが、これは最初のモジュラー チェック (強制的に %eax にする) を実行することで判断できます。または、現時点では、eax になる予定です (あるべきです)。

ループを次のように記述できます (workingNum が eax にあり、これが 32 ビット AT&T 構文であると仮定すると、64 ビット アセンブリまたは Intel 構文がわからないためです)。

asm( "
     movl     $3, %ebx
  WorkNumMod3Loop: 
     movl     %eax, %ecx    # just to be safe, backup workingNUm
     movl     $0, %edx      # zero out edx
     idivl    $3            # divide by 3. quotient in eax, remainder in edx
     cmpl     $0, %edx      # compare if it's 0
     jne      AfterNumMod3Loop    # if 0 is the remainder, jump out

     # no need to perform division because new workingNum is already in eax
     #factors.push_back(3) call

     je       WorkNumMod3Loop
  AfterNumMod3Loop: 
     movl     %ecx, %eax"
);

これらのループのアセンブリ出力を確認する必要があります。コンパイラがすでにこれらの最適化を行っている可能性がありますが、私はそれを疑っています。メソッド呼び出しの前に workingNum /= n を配置すると、パフォーマンスが少し向上する場合があります。

于 2010-10-13T21:40:41.870 に答える
2

Omnifarious からいくつかのアイデアを取り入れ、その他の改善を行いました。

// system specific typedef for ulong should go here (or use boost::uint64_t)
typedef unsigned __int64 ulong;
typedef std::vector<ulong> ULongVector;

// Caller needs to pass in an empty factors vector
void GetFactors(ULongVector &factors, ulong num)
{
  if (num<2)
    return;

  ulong workingNum=num;

  // Factor out factors of 2
  while (workingNum%2==0)
  {
    factors.push_back(2);
    workingNum/=2;
  }

  // Factor out factors of 3
  while (workingNum%3==0)
  {
    factors.push_back(3);
    workingNum/=3;
  }

  if (workingNum==1)
    return;

  // Factor out factors >=5
  ulong nextOffset=2;
  char nextShift = 1;
  ulong n = 5;
  ulong nn = 25;
  do {
    // Is workingNum divisible by n?
    if (workingNum%n==0)
    {
      // n is a factor!
      // so is the number multiplied by n to get workingNum

      // Insert n into the list of factors
      factors.push_back(n);

      // Divide working number by n
      workingNum/=n;

      // Test for done...
      if (workingNum==1)
        return;

      // Try n again
    }  
    else {
      nn += (n << (nextShift+1)) + (1<<(nextShift*2)); // (n+b)^2 = n^2 + 2*n*b + b*2
      n += nextOffset;
      nextOffset ^= 6;
      nextShift ^= 3;
      // invariant: nn == n*n
      if (n & 0x100000000LL) break; // careful of integer wraparound in n^2
    }
  } while (nn <= workingNum);

  // workingNum is prime, add it to the list of factors        
  factors.push_back(workingNum);
}
于 2010-10-12T21:37:55.853 に答える
2

自然な一般化は、2 と 3 だけでなく、より多くの既知の素数を使用してスキップページを事前計算することです。2, 3, 5, 7, 11 のように、2310 のパターン期間 (ハァッ、ナイス ナンバー)。おそらくそれ以上ですが、利益は減少します.実行時間のグラフは、事前計算が悪影響を及ぼし始める場所を正確に確立できますが、もちろん、因数分解する数値の数に依存します...

コーディングの詳細は皆さんにお任せします。:-)

乾杯 & hth.,

– アルフ

于 2010-10-13T04:00:25.313 に答える