С.И.Хашин
Простых чисел, меньших 1011 (100 млрд.) меньше, чем 232, поэтому для хранения номеров таких простых чисел достаточно 4-х байт. На самом деле,
π( 104.4 ⋅ 109 )= 4 291 624 115 = 232 - 3343181.а значит, π( 104.4 ⋅ 109 ) ≈ 232.
Заметим, что среди вычетов по модулю 30 ровно 8 взаимно просты с 30, это ±1, ±7, ±11, ±13. Поэтому удобно будет хранить в байте номер k признаки простоты чисел
30k+1, 30k+7, 30k+11, 30k+13, 30k+17, 30k+19, 30k+23, 30k+29Выбираем некоторую границу R, например, R=1011, решето Эратосфена будет занимать R/30 байт. Далее, выбираем ещё одну границу R1, например R1 = R/500. Построим массив целых pr размером R1/2 чисел такой, что:
pr[k]=номер простого числа m, если оно простое и -( наименьший простой делитель m) иначе, где m=2k+1.Кроме того, построим массив unsigned *pi размера π(R1) состоящий из всех простых чисел, меньших R1.
i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 2*i+1: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 pr[i]: 0 2 3 4 -3 5 6 -3 7 8 -3 9 -5 -3 10 11 -3 -5 12 -3 pi[i]: 0 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67
Благодаря этому, для всех нечётных чисел, меньших R1 через одно обращение к таблице находится наибольший простой делитель числа.
Таким образом, в максимальном варианте, для хранения всех простых чисел, меньших 100 млрд.
нам потребуется 3Гбайта оперативной памяти и ещё 400М байт для хранения наибольших простых делителей
чисел, меньших 200 млн..
При современных объемах оперативной памяти — вполне умеренные требования!
Для реализации этого предлагается набор функций для работы с небольшими (до 264,
для криптографии - недостаточно) простыми числами. Они организованы в виде статических
членов класса z64 (динамических членов у класса нет).
Модуль состоит из двух файлов:
z64.h - заголовочный файл
z64.cpp - реализация.
В модуле не используются никакие внешние библиотеки.
Единственная сложность – использование ассемблера в функции mul_mod.
Если вы не хотете с этим связываться, ассемблер можно отключить, все вычисления
будут идти на С++. При этом функция mul_mod и связанные с ней (типа pow_mod) будут
работать в 3-4 раза медленнее.
Основные методы класса:
typedef unsigned long long u64; typedef long long i64; class z64 { ... public: static u64 maxN(); // return maximal N static u64 maxP(); // return maximal prime in the sieve // Primes static bool isPrime( u64 n); // is prime(n)? static u64 p ( u64 i); // i-th prime, Maple: ithprime(i), p(0)=0, p(1)=2, p(2)=3,... static u64 ip( u64 n); // index of prime number, 0 if n is composite static u64 Pi( u64 n); // the number of primes ≤ n, Pi(0)=Pi(1)=0, Pi(2)=1, Pi(3)=Pi(4)=2,... static u64 NextPrime( u64 n); // prime p>n, NextPrime(0)=NextPrime(1)=2, NextPrime(2)=3, NextPrime(3)=NextPrime(4)=5. static u64 PrevPrime( u64 n); // prime p<n, PrevPrime(0)=PrevPrime(1)=PrevPrime(2)=0, PrevPrime(4)=3 // Factoring static u64 primeFactor( u64 n);// prime divisor or 1, if not found static bool isFullSq (u64 n); // n is full square? static u64 gcd (u64 a, u64 b); // GCD(a,b) static u64 mul_mod (u64 a, u64 b, u64 n); // (a*b)%c static u64 pow_mod (u64 a, u64 b, u64 c); // (a^b)%c static bool MillerRabin(u64 a,u64 n); // MR primality test for n with base a static void euclid (u64 a, u64 b, i64 &x, i64 &y, u64 &d); // find x,y,d: a*x+b*y=d=gcd(a,b) static u64 inverse (u64 a, u64 n); // 1/a mod n static bool chineese (u64 mod1, u64 b1, u64 mod2, u64 b2, u64 &mod3, u64 &b3); // cheneese theorem // Rest static u64 order( u64 a, u64 n); // order of a by modulo prime n static int jacobi(u64 a, u64 n); // Jacobi symbol static char *factorString(u64 n); // string with factorisation: 180200 -> 2^3*5^2*17*53 static u64 msqrt( u64 a, u64 p); // sqrt(a) mod p, p must be prime! // Frobenius static void Frobenius( u64 a, u64 b, i64 c, u64 k, u64 p, u64 &a1, u64 &b1); // a1+b1*sqrt(c) = (a+b*sqrt(c))^k mod p static u64 FrobOrder( u64 a, u64 b, i64 c, u64 p); // order(a+b*sqrt(c)) mod p static bool FrobTest ( u64 a, u64 b, i64 c, u64 n); // n is prime? static int minC(u64 n); // minimal c: J(c/n)=-1; static void toString(char *s, u64 n, int w=0); // s <- to String of width w static char *toString(u64 n, int w=0); // return to String of width w };Настройка пакета на различные процессоры и компиляторы сводится к корректировке двух строк в заголовочном файле:
typedef unsigned long long u64; typedef long long i64;В ней надо указать название типа 64-битовых целых знаковых и беззнаковых чисел.
mul_mod
Это - ключевая функция пакета. Реализация ее на ассемблере ускоряет работу
более, чем на порядок. Ассемблерная реализация проверена лишь для gcc-64 под Linux.
u64 z64::mul_mod(u64 a, u64 b, u64 c) // (a*b)%c { #if defined(_MSC_VER) || defined( _WIN32 ) u64 res=0; if(a>c) a %= c; while(b){ if(b&1) {res+=a; if(res>c) res-=c; b--; } else { a<<=1; if(a >c) a -=c; b>>=1;} } return res; #else u64 d; // to hold the result of a*b mod c // calculates a*b mod c, stores result in d asm ("mov %1, %%rax;" // put a into rax "mul %2;" // mul a*b -> rdx:rax "div %3;" // (a*b)/c -> quot in rax remainder in rdx "mov %%rdx, %0;" // store result in d :"=r"(d) // output :"r"(a), "r"(b), "r"(c) // input :"%rax", "%rdx" // clobbered registers ); return d; #endif } // z64::mul_mod
Под Visual C++/64 функция также реализована на ассемблере в модуле mul_mod.asm:
; extern "C" u64 mul_mod_ams(u64 a, u64 b, u64 n); ; Project -> Properties -> Build Dependences -> Build Customizations.. -> пометить masm ; mul_mod.asm->Properties->(Platform=x64) Microsoft Macro Assembler->Preprocessor Definition->_WIN64 ; команда mul умножает регистр rax на другой регистр IFDEF _WIN64 ; В 32-разрядном режиме не исользуется .CODE mul_mod_asm PROC ; rcx = a ; rdx = b ; r8 = n mov rax, rdx ; put b into rax mul rcx ; mul a*b -> rdx:rax div r8 ; (a*b)/n -> quot in rax remainder in rdx mov rax, rdx ; Копирование RDX в RAX ret ; Возврат в главную программу mul_mod_asm ENDP ENDIF END
primeFactor:
static u64 primeFactor( u64 n); // prime divisor or 1, if not found
По результатам некоторых исследований и большого числа экспериментов в наших пределах (до 264) из элементарных методов наиболее эффективен оказался ρ-метод Полларда, все остальные не стоит и реализовывать. Если все простые множители у числа велики (несколько миллиардов), то методу может потребоваться до 2-3 сотен тысяч шагов. Но остальные методы - все равно ещё медленнее. Но, например, разложение числа p-1 на множители для простого p (это используется в функции order) находится без труда.
factorString:
static void toString(char *s, u64 n, int w=0); // s >- to String of width w static char *toString(u64 n, int w=0); // return to String of width w
Эта функция использует всегда одну и ту же статическую область памяти для формирования строки. То есть возвращаемый указатель - всегда один и тот же. Память не надо не выделять, ни освобождать. Надо только не забывать, что при повторном вызове этой функции, она снова использует ту же самую память. Если требуется преобразовать в строки сразу несколько чисел, то можно использовать функцию
static void toString(char *s, u64 n, int w=0); // s <- to String of width wНо о выделении достаточной памяти тогда уж придется заботится самому.
Пример использования:
#include "z64.h" ... z64::Efill(1<<24); // fill the sieve till 2^24 z64::Esave("16M.sieve"); // save the sieve u64 p = z64::p(1000000); // p = 15485863 u64 i = z64::Pi(p); // i = 1000000 u64 p2= z64::p( 700000); // p = 10570841 u64 n = p*p2; // n = 163 698 595 520 783 bool r = z64::isPrime(n); // r = false u64 f = z64::primeFactor(n); // f = 10570841
В основном, модуль разработан для учебных целей, и скорость работы не являлась главной целью. Однако при размере чисел в 64 бита, все равно вычисления получаются более быстрыми,чем при использовании библиотеки GMP. Например операция mul_mod на процессоре AMD Athlon II X2 240, 2.8 GHz занимает для 63-битовых чисел:
разрядность | язык | время, mks |
---|---|---|
32 | C++ | 75 |
64 | C++ | 31 |
64 | asm | 2.7 |
64 | GMP | 7 |