Перейти до основного вмісту

Множення Монтгомері

Багато алгоритмів теорії чисел, як-от перевірка на простоту чи факторизація цілих чисел, а також алгоритми криптографії, на кшталт RSA, потребують безлічі операцій за модулем великого числа. Множення на кшталт xymodnx y \bmod{n} обчислюється доволі повільно за допомогою звичних алгоритмів, оскільки воно вимагає ділення, щоб дізнатися, скільки разів треба відняти nn від добутку. А ділення — це справді дорога операція, особливо коли йдеться про великі числа.

Множення (за модулем) Монтгомері — це метод, який дозволяє обчислювати такі множення швидше. Замість того, щоб ділити добуток і багаторазово віднімати nn, він додає кратні nn, щоб занулити молодші біти, а потім просто відкидає ці молодші біти.

Коли підходить цей алгоритм?
  • Чи виконується багато множень за тим самим модулем nn (наприклад, у тестах простоти чи факторизації)? Лише так одноразова вартість переходу в простір Монтгомері окупається.
  • Чи модуль nn непарний, тобто gcd(n,2m)=1\gcd(n, 2^m) = 1? Для парного nn вибір r=2mr = 2^m не працює.
  • Чи вузьке місце саме в операції за модулем (ділення), яку хочеться замінити зсувами? Якщо множень мало, простіше обійтися звичайним %.

Представлення Монтгомері

Однак множення Монтгомері не дається задарма. Алгоритм працює лише в просторі Монтгомері. І нам потрібно перетворити наші числа в цей простір, перш ніж починати множити.

Для цього простору нам потрібне додатне ціле число rnr \ge n, взаємно просте з nn, тобто gcd(n,r)=1\gcd(n, r) = 1. На практиці ми завжди вибираємо rr рівним 2m2^m для додатного цілого mm, оскільки тоді множення, ділення та операції за модулем rr можна ефективно реалізувати за допомогою зсувів та інших бітових операцій. nn майже в усіх застосуваннях буде непарним числом, бо парне число факторизувати неважко. Тож кожен степінь 22 буде взаємно простим з nn.

Представник xˉ\bar{x} числа xx у просторі Монтгомері означається як:

xˉ:=xrmodn\bar{x} := x \cdot r \bmod n

Зауважте, що це перетворення насправді і є тим самим множенням, яке ми хочемо оптимізувати. Тож це все ще дорога операція. Однак перетворити число в простір потрібно лише один раз. Щойно ви опинилися в просторі Монтгомері, ви можете ефективно виконувати скільки завгодно операцій. А наприкінці ви перетворюєте остаточний результат назад. Тож поки ви виконуєте багато операцій за модулем nn, це не буде проблемою.

Усередині простору Монтгомері ви все ще можете виконувати більшість операцій як звичайно. Ви можете додавати два елементи (xr+yr(x+y)rmodnx \cdot r + y \cdot r \equiv (x + y) \cdot r \bmod n), віднімати, перевіряти рівність і навіть обчислювати найбільший спільний дільник числа з nn (оскільки gcd(n,r)=1\gcd(n, r) = 1). Усе це за допомогою звичних алгоритмів.

Однак для множення це не так.

Ми очікуємо, що результатом буде:

xˉyˉ=xy=(xy)rmodn.\bar{x} * \bar{y} = \overline{x \cdot y} = (x \cdot y) \cdot r \bmod n.

Але звичайне множення дасть нам:

xˉyˉ=(xy)rrmodn.\bar{x} \cdot \bar{y} = (x \cdot y) \cdot r \cdot r \bmod n.

Тому множення в просторі Монтгомері означається так:

xˉyˉ:=xˉyˉr1modn.\bar{x} * \bar{y} := \bar{x} \cdot \bar{y} \cdot r^{-1} \bmod n.

Редукція Монтгомері

Множення двох чисел у просторі Монтгомері вимагає ефективного обчислення xr1modnx \cdot r^{-1} \bmod n. Ця операція називається редукцією Монтгомері і також відома як алгоритм REDC.

Оскільки gcd(n,r)=1\gcd(n, r) = 1, ми знаємо, що існують два числа r1r^{-1} та nn^{\prime} з 0<r1,n<n0 < r^{-1}, n^{\prime} < n такі, що

rr1+nn=1.r \cdot r^{-1} + n \cdot n^{\prime} = 1.

Обидва числа r1r^{-1} та nn^{\prime} можна обчислити за допомогою розширеного алгоритму Евкліда.

Користуючись цією тотожністю, ми можемо записати xr1x \cdot r^{-1} як:

xr1=xrr1/r=x(nn+1)/r=(xnn+x)/r(xnn+lrn+x)/rmodn((xn+lr)n+x)/rmodn\begin{aligned} x \cdot r^{-1} &= x \cdot r \cdot r^{-1} / r = x \cdot (-n \cdot n^{\prime} + 1) / r \\ &= (-x \cdot n \cdot n^{\prime} + x) / r \equiv (-x \cdot n \cdot n^{\prime} + l \cdot r \cdot n + x) / r \bmod n\\ &\equiv ((-x \cdot n^{\prime} + l \cdot r) \cdot n + x) / r \bmod n \end{aligned}

Ці еквівалентності виконуються для будь-якого цілого ll. Це означає, що ми можемо додавати чи віднімати довільне кратне rr до xnx \cdot n^{\prime}, або, іншими словами, ми можемо обчислити q:=xnq := x \cdot n^{\prime} за модулем rr.

Це дає нам такий алгоритм для обчислення xr1modnx \cdot r^{-1} \bmod n:

function reduce(x):
q = (x mod r) * n' mod r
a = (x - q * n) / r
if a < 0:
a += n
return a

Оскільки x<nn<rnx < n \cdot n < r \cdot n (навіть якщо xx є добутком множення) і qn<rnq \cdot n < r \cdot n, ми знаємо, що n<(xqn)/r<n-n < (x - q \cdot n) / r < n. Тому остаточна операція за модулем реалізується за допомогою однієї перевірки та одного додавання.

Як ми бачимо, ми можемо виконати редукцію Монтгомері без жодних важких операцій за модулем. Якщо ми оберемо rr як степінь 22, то операції за модулем та ділення в алгоритмі можна обчислити за допомогою бітових масок і зсувів.

Другим застосуванням редукції Монтгомері є перенесення числа назад із простору Монтгомері у звичайний простір.

Трюк зі швидким оберненням

Щоб ефективно обчислити обернений елемент n:=n1modrn^{\prime} := n^{-1} \bmod r, ми можемо скористатися таким трюком (натхненним методом Ньютона):

ax1mod2kax(2ax)1mod22ka \cdot x \equiv 1 \bmod 2^k \Longrightarrow a \cdot x \cdot (2 - a \cdot x) \equiv 1 \bmod 2^{2k}

Це легко довести. Якщо ми маємо ax=1+m2ka \cdot x = 1 + m \cdot 2^k, то маємо:

ax(2ax)=2ax(ax)2=2(1+m2k)(1+m2k)2=2+2m2k12m2km222k=1m222k1mod22k.\begin{aligned} a \cdot x \cdot (2 - a \cdot x) &= 2 \cdot a \cdot x - (a \cdot x)^2 \\ &= 2 \cdot (1 + m \cdot 2^k) - (1 + m \cdot 2^k)^2 \\ &= 2 + 2 \cdot m \cdot 2^k - 1 - 2 \cdot m \cdot 2^k - m^2 \cdot 2^{2k} \\ &= 1 - m^2 \cdot 2^{2k} \\ &\equiv 1 \bmod 2^{2k}. \end{aligned}

Це означає, що ми можемо почати з x=1x = 1 як оберненого до aa за модулем 212^1, застосувати трюк кілька разів, і на кожній ітерації ми подвоюватимемо кількість правильних бітів числа xx.

Реалізація

За допомогою компілятора GCC ми все ще можемо ефективно обчислити xymodnx \cdot y \bmod n, коли всі три числа є 64-бітними цілими, оскільки компілятор підтримує 128-бітні цілі через типи __int128 та __uint128.

long long result = (__int128)x * y % n;

Однак для 256-бітних цілих типу немає. Тому тут ми покажемо реалізацію для 128-бітного множення.

using u64 = uint64_t;
using u128 = __uint128_t;
using i128 = __int128_t;

struct u256 {
u128 high, low;

static u256 mult(u128 x, u128 y) {
u64 a = x >> 64, b = x;
u64 c = y >> 64, d = y;
// (a*2^64 + b) * (c*2^64 + d) =
// (a*c) * 2^128 + (a*d + b*c)*2^64 + (b*d)
u128 ac = (u128)a * c;
u128 ad = (u128)a * d;
u128 bc = (u128)b * c;
u128 bd = (u128)b * d;
u128 carry = (u128)(u64)ad + (u128)(u64)bc + (bd >> 64u);
u128 high = ac + (ad >> 64u) + (bc >> 64u) + (carry >> 64u);
u128 low = (ad << 64u) + (bc << 64u) + bd;
return {high, low};
}
};

struct Montgomery {
Montgomery(u128 n) : mod(n), inv(1) {
for (int i = 0; i < 7; i++)
inv *= 2 - n * inv;
}

u128 init(u128 x) {
x %= mod;
for (int i = 0; i < 128; i++) {
x <<= 1;
if (x >= mod)
x -= mod;
}
return x;
}

u128 reduce(u256 x) {
u128 q = x.low * inv;
i128 a = x.high - u256::mult(q, mod).high;
if (a < 0)
a += mod;
return a;
}

u128 mult(u128 a, u128 b) {
return reduce(u256::mult(a, b));
}

u128 mod, inv;
};

Швидке перетворення

Поточний метод перетворення числа в простір Монтгомері доволі повільний. Є швидші способи.

Можна помітити таке співвідношення:

xˉ:=xrmodn=xr2/r=xr2\bar{x} := x \cdot r \bmod n = x \cdot r^2 / r = x * r^2

Перетворення числа в простір — це просто множення всередині простору цього числа на r2r^2. Тому ми можемо заздалегідь обчислити r2modnr^2 \bmod n і просто виконати одне множення замість того, щоб зсувати число 128 разів.

У наведеному нижче коді ми ініціалізуємо r2 значенням -n % n, що еквівалентно rnrmodnr - n \equiv r \bmod n, і зсуваємо його 4 рази, щоб отримати r24modnr \cdot 2^4 \bmod n. Це число можна інтерпретувати як 242^4 у просторі Монтгомері. Якщо ми піднесемо його до квадрата 55 разів, то отримаємо (24)25=(24)32=2128=r(2^4)^{2^5} = (2^4)^{32} = 2^{128} = r у просторі Монтгомері, що є саме r2modnr^2 \bmod n.

struct Montgomery {
Montgomery(u128 n) : mod(n), inv(1), r2(-n % n) {
for (int i = 0; i < 7; i++)
inv *= 2 - n * inv;

for (int i = 0; i < 4; i++) {
r2 <<= 1;
if (r2 >= mod)
r2 -= mod;
}
for (int i = 0; i < 5; i++)
r2 = mul(r2, r2);
}

u128 init(u128 x) {
return mult(x, r2);
}

u128 mod, inv, r2;
};