С.И.Хашин
Простых чисел, меньших 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 |