Грубая сила, однопоточная первичная факторизация - PullRequest
16 голосов
/ 13 октября 2010

На рассмотрение предлагается следующая функция, которую можно использовать (относительно быстро) для деления 64-разрядного целого числа без знака на его основные множители.Обратите внимание, что факторинг не является вероятностным (т. Е. Точным).Алгоритм уже достаточно быстр, чтобы обнаружить, что число является простым или имеет несколько очень больших факторов в течение нескольких секунд, на современном оборудовании.

Вопрос: можно ли внести какие-либо улучшения в представленный алгоритм, сохраняя его однопоточным, чтобы он мог вычислять (произвольно) очень большие 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);
}

Спасибо

Редактировать: я добавил еще больше комментариев.Причина, по которой вектор передается по ссылке, заключается в том, что он позволяет повторно использовать вектор между вызовами и избегать динамического распределения.Причина, по которой вектор не очищен в функции, заключается в том, что он допускает нечетное требование добавления текущих «числовых» факторов к факторам, уже имеющимся в векторе.

Сама функция не изящна и может быть реорганизована, но вопрос в том, как сделать алгоритм быстрее.Поэтому, пожалуйста, никаких предложений о том, как сделать функцию более красивой, читаемой или C ++.Это детская игра.Трудно усовершенствовать этот алгоритм, чтобы он мог быстрее найти (проверенные) факторы.

Обновление: Potatoswatter пока имеет несколько отличных решений, обязательно ознакомьтесь с его MMX-решением рядомдно, а также.

Ответы [ 7 ]

19 голосов
/ 14 октября 2010

Сравните такой подход с (предварительно созданным) ситом. Модуль по модулю дорог, поэтому оба подхода по существу делают две вещи: генерируют потенциальные факторы и выполняют операции по модулю. Любая из программ должна разумно сгенерировать новый фактор-кандидат за меньшее количество циклов, чем по модулю, поэтому любая из программ связана по модулю.

Данный подход отфильтровывает постоянную пропорцию всех целых чисел, а именно, кратные 2 и 3, или 75%. Каждое четвертое (как указано) число используется в качестве аргумента оператора по модулю. Я назову это пропускающим фильтром.

С другой стороны, сито использует только простые числа в качестве аргументов оператора по модулю, а средняя разница между последовательными простыми числами определяется теоремой простого числа , равной 1 / Ln (N). Например, e ^ 20 составляет чуть менее 500 миллионов, так что у чисел более 500 миллионов менее 5% шансов быть простыми. Если учитываются все числа до 2 ^ 32, хорошее правило 5%.

Поэтому сито будет тратить в 5 раз меньше времени на операции div в качестве фильтра пропуска. Следующим фактором, который необходимо учитывать, является скорость, с которой сито производит простые числа, то есть считывает их из памяти или с диска. Если выборка одного простого происходит быстрее, чем 4 div с, то сито быстрее. Согласно моим таблицам пропускная способность div на моем Core2 составляет не более одного на 12 циклов. Это будут сложные проблемы с делением, поэтому давайте составим 50 циклов на простое число. Для процессора с частотой 2,5 ГГц это 20 наносекунд.

За 20 нс жесткий диск со скоростью 50 МБ / с может считывать около одного байта. Простое решение - использовать 4 байта на простое число, поэтому диск будет работать медленнее. Но мы можем быть умнее. Если мы хотим закодировать все простые числа по порядку, мы можем просто закодировать их различия. Опять же, ожидаемая разница составляет 1 / ln (N). Кроме того, они все четные, что экономит лишний бит. И они никогда не равны нулю, что делает расширение многобайтовой кодировки бесплатным. Таким образом, используя один байт на простое число, различия до 512 могут быть сохранены в одном байте, что дает нам до 303371455241 в соответствии с той статьей Википедии .

Следовательно, в зависимости от жесткого диска, сохраненный список простых чисел должен быть примерно одинаковым по скорости при проверке простоты. Если он может быть сохранен в ОЗУ (это 203 МБ, поэтому последующие запуски, вероятно, попадут в кэш-память диска), тогда проблема полностью исчезнет, ​​поскольку скорость FSB обычно отличается от скорости процессора на коэффициент, меньший ширины FSB в байтах. То есть FSB может передавать более одного простого числа за цикл. Тогда фактором улучшения является сокращение операций подразделения, то есть в пять раз. Это подтверждается экспериментальными результатами ниже.

Конечно, тогда есть многопоточность. Диапазоны простых чисел или пропущенных кандидатов могут быть назначены разным потокам, что делает любой подход смущающе параллельным. Нет оптимизаций, которые не включают увеличение числа параллельных цепей делителей, если только вы не исключите модуль по модулю.

Вот такая программа. Он настроен так, что вы можете добавить bignums.

/*
 *  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 ГГц 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
9 голосов
/ 15 октября 2010

Мой другой ответ довольно длинный и сильно отличается от этого, так что вот еще кое-что.

Вместо того, чтобы просто отфильтровывать кратные числа первых двух простых чисел или кодировать все соответствующие простые числа в один байт каждый, эта программа отфильтровывает кратные значения всех простых чисел, которые умещаются в восемь битов, в частности от 2 до 211. Таким образом, вместо передачи 33% номеров, это передает около 10% оператору деления.

Простые числа хранятся в трех регистрах SSE, а их модули с счетчиком хода сохраняются в следующих трех. Если модуль любого простого числа со счетчиком равен нулю, счетчик не может быть простым. Кроме того, если любой модуль равен единице, то (counter + 2) не может быть простым, и т. Д., До (counter + 30). Четные числа игнорируются, а смещения, такие как +3, +6 и +5, пропускаются. Векторная обработка позволяет обновлять шестнадцать байтовых переменных одновременно.

После того, как я добавил к этому кухонную раковину с полной микрооптимизацией (но не более специфичной для платформы, чем встроенная директива), я получил увеличение производительности в 1,78 раза (24 с против 13,4 с на моем ноутбуке). При использовании библиотеки 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

Ниже приведен первый набросок вышеперечисленного. Оптимизация включена

  • Сделать слияние циклического счетчика безусловным (избегайте ветвления).
  • Получить ILP, развернув петлю в 15 раз, увеличив шаг до 30.
    • Вдохновлен вашей оптимизацией.
    • 30 кажется приятным местом, так как он удаляет кратные 2, 3 и 5 бесплатно.
    • Простые числа от 5 до 15 могут иметь несколько кратных за один шаг, поэтому поместите несколько копий в разную фазу в векторе.
  • Фактор вне remove_factor.
  • Изменение условных, непредсказуемых remove_factor обращений к записи без ветвления массива.
  • Полностью разверните последний цикл с помощью вызова remove_factor и убедитесь, что функция всегда встроена.
    • Исключите последнюю развернутую итерацию, поскольку среди кандидатов всегда есть кратное число 7.
  • Добавьте еще один вектор, содержащий все оставшиеся простые числа, которые достаточно малы.
  • Увеличьте пространство, добавив смещение к счетчикам, и добавьте еще один вектор. Теперь есть только шесть простых чисел, которые могут быть отфильтрованы без увеличения до 16 бит, и я также исчерпал регистры: циклу нужны 3 простых вектора, 3 вектора модуля, 8 констант для поиска и по одной константе каждый увеличить на и сделать проверку диапазона. Это составляет 16.
    • В этом приложении усиление минимальное (но положительное), но первоначальная цель этого метода состояла в том, чтобы отфильтровать простые числа для сита в другом ответе. Так что следите за обновлениями ...

Читаемая версия:

/*
 *  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;
}
5 голосов
/ 16 октября 2010

Метод факторизации Ферма прост и быстр для поиска пар больших простых факторов, если вы остановите его, пока он не зашел слишком далеко и не стал медленным.Однако в моих тестах на случайных числах такие случаи были слишком редкими, чтобы увидеть какие-либо улучшения.

... без использования вероятностного подхода (например, Миллера-Рабина) для определения примитивности

При равномерном распределении 75% ваших входных данных потребуется миллиард итераций цикла, поэтому сначала стоит потратить миллион операций на менее детерминированные методы, даже если вы получите неубедительный ответ и должны вернуться к пробному разделению.

Я нашел вариант Брента метода Ро Полларда очень хорошим, хотя и более сложным для кодирования и понимания.Лучший пример, который я видел, это обсуждение на форуме .Этот метод основывается на удаче, но помогает достаточно часто, чтобы быть стоящим.

Тест первичности Миллера-Рабина на самом деле является детерминированным, вплоть до 10 ^ 15, что может избавить вас от необходимости бесполезного поиска.

Я попробовал несколько десятков вариантов и остановился на следующем для факторизации значений int64:

  1. Пробное деление на малые факторы.(Я использую первые 8000 предварительно вычисленных простых чисел.)
  2. 10 попыток с Rho Полларда, каждая из которых использует 16 итераций
  3. Пробное деление на sqrt (n).

Обратите внимание, чтоRho Полларда находит факторы, которые не обязательно являются первичными, поэтому для их учета можно использовать рекурсию.

2 голосов
/ 14 октября 2010

Я не уверен, насколько эффективными они были бы, но вместо

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.Проблема в том, что вы снова выполняете деление (и я сомневаюсь, что компилятор видит, что вы просто неявно выполнили деление в состоянии цикла).Таким образом, вы могли бы встроить это.Это создает две проблемы:

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 перед вызовом метода в некоторых случаях немного повышает производительность.

2 голосов
/ 13 октября 2010

Естественное обобщение состоит в том, чтобы предварительно вычислять лыжные трассы, используя больше известных простых чисел, чем просто 2 и 3. Как 2, 3, 5, 7, 11, для периода шаблона 2310 (да, хорошее число).И, возможно, больше, но он имеет убывающую отдачу - график времени выполнения мог бы точно определить, где предварительное вычисление начинает оказывать отрицательное влияние, но, конечно, это зависит от числа чисел, которые нужно учитывать ...

ХаЯ оставляю вам подробности кодирования.: -)

Приветствия и hth.,

- Alf

2 голосов
/ 13 октября 2010

Включая некоторые идеи от 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);
}
2 голосов
/ 13 октября 2010

Этот код довольно медленный, и я уверен, что понимаю почему. Это не невероятно медленно, но определенно медленнее в диапазоне 10-20%. Деление не должно выполняться один раз для каждого проходного цикла, но единственный способ сделать это - вызвать 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);
  }
}
...