Multiplicación de Montgomery
Muchos algoritmos de teoría de números, como el test de primalidad o la factorización de enteros, y en criptografía, como RSA, requieren muchas operaciones módulo un número grande. Una multiplicación como es bastante lenta de calcular con los algoritmos típicos, porque hace falta una división para saber cuántas veces hay que restar del producto. Y la división es una operación realmente costosa, sobre todo con números grandes.
La multiplicación (modular) de Montgomery es un método que permite calcular esas multiplicaciones más rápido. En lugar de dividir el producto y restar varias veces, suma múltiplos de para anular los bits bajos y luego simplemente descarta los bits bajos.
Representación de Montgomery
Sin embargo, la multiplicación de Montgomery no es gratis. El algoritmo solo funciona en el espacio de Montgomery. Y hay que transformar nuestros números a ese espacio antes de poder empezar a multiplicar.
Para el espacio necesitamos un entero positivo coprimo con , es decir, . En la práctica siempre elegimos como para un entero positivo , porque entonces las multiplicaciones, divisiones y operaciones módulo se pueden implementar de forma eficiente con desplazamientos y otras operaciones de bits. será un número impar en prácticamente todas las aplicaciones, porque no es difícil factorizar un número par. Así que toda potencia de será coprima con .
El representante de un número en el espacio de Montgomery se define como:
Nótese que la transformación es, de hecho, justamente una multiplicación de las que queremos optimizar. Así que sigue siendo una operación costosa. Sin embargo, solo hace falta transformar un número una vez al espacio. En cuanto estamos en el espacio de Montgomery, podemos realizar tantas operaciones como queramos de forma eficiente. Y al final transformamos el resultado final de vuelta. Así que, mientras se hagan muchas operaciones módulo , esto no será un problema.
Dentro del espacio de Montgomery todavía se pueden realizar la mayoría de las operaciones como de costumbre. Se pueden sumar dos elementos (), restar, comprobar igualdad e incluso calcular el máximo común divisor de un número con (porque ). Todo con los algoritmos habituales.
Sin embargo, esto no vale para la multiplicación.
Esperamos que el resultado sea:
Pero la multiplicación normal nos da:
Por lo tanto, la multiplicación en el espacio de Montgomery se define como:
Reducción de Montgomery
La multiplicación de dos números en el espacio de Montgomery requiere un cálculo eficiente de . Esta operación se llama reducción de Montgomery, y también se conoce como el algoritmo REDC.
Como , sabemos que existen dos números y con tales que
Tanto como se pueden calcular usando el algoritmo de Euclides extendido.
Usando esta identidad podemos escribir como:
Las equivalencias valen para cualquier entero arbitrario . Esto significa que podemos sumar o restar un múltiplo arbitrario de a , o en otras palabras, podemos calcular módulo .
Esto nos da el siguiente algoritmo para calcular :
function reduce(x):
q = (x mod r) * n' mod r
a = (x - q * n) / r
if a < 0:
a += n
return aComo (incluso si es el producto de una multiplicación) y , sabemos que . Por lo tanto, la operación de módulo final se implementa con una sola comprobación y una suma.
Como se ve, podemos realizar la reducción de Montgomery sin ninguna operación de módulo pesada. Si elegimos como una potencia de , las operaciones de módulo y las divisiones del algoritmo se pueden calcular con máscaras de bits y desplazamientos.
Una segunda aplicación de la reducción de Montgomery es transferir un número de vuelta del espacio de Montgomery al espacio normal.
Truco del inverso rápido
Para calcular el inverso de forma eficiente, podemos usar el siguiente truco (inspirado en el método de Newton):
Esto se puede demostrar fácilmente. Si tenemos , entonces:
Esto significa que podemos empezar con como inverso de módulo , aplicar el truco unas cuantas veces y en cada iteración duplicamos la cantidad de bits correctos de .
Implementación
Con el compilador GCC todavía podemos calcular de forma eficiente cuando los tres números son enteros de 64 bits, porque el compilador soporta enteros de 128 bits con los tipos __int128 y __uint128.
long long result = (__int128)x * y % n;Sin embargo, no hay un tipo para enteros de 256 bits. Por eso aquí mostramos una implementación para una multiplicación de 128 bits.
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;
};Transformación rápida
El método actual de transformar un número al espacio de Montgomery es bastante lento. Hay formas más rápidas.
Se puede observar la siguiente relación:
Transformar un número al espacio es simplemente una multiplicación dentro del espacio del número por . Por lo tanto, podemos precomputar y realizar una multiplicación en lugar de desplazar el número 128 veces.
En el código siguiente inicializamos r2 con -n % n, que es equivalente a , y lo desplazamos 4 veces para obtener .
Este número se puede interpretar como en el espacio de Montgomery.
Si lo elevamos al cuadrado veces, obtenemos en el espacio de Montgomery, que es exactamente .
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;
};