multiplicação modular

7

Eu estava lendo a página Modular Multiplication na wikipedia ... e não conseguia entender o algoritmo para calcular ab(modm) .

uint64_t mul_mod(uint64_t a, uint64_t b, uint64_t m)
{
  long double x;
  uint64_t c;
  int64_t r;
  if (a >= m) a %= m;
  if (b >= m) b %= m;
  x = a;
  c = x * b / m;
  r = (int64_t)(a * b - c * m) % (int64_t)m;
  return r < 0 ? r + m : r;
}

Entendi:

ab(modm)((amodm)(bmodm))modm

o passo que eu não consegui é c=x*b/me depois fazer o r = (int64_t)(a * b - c * m) % (int64_t)m;que é isso?

O artigo diz:

empregando o truque que, por hardware, a multiplicação de ponto flutuante resulta nos bits mais significativos do produto mantidos, enquanto a multiplicação inteira resulta nos bits menos significativos mantidos

Não vejo como isso está relacionado !!!

usuário -1
fonte

Respostas:

7

Aqui está uma descrição mais compacta e matemática do que está acontecendo. Deixe e ser a entrada, já reduzido módulo , de modo que e . (Em termos de código, isso significa depois da linha.) Queremos calcular , ou seja, definindo , queremos encontrar modo que para alguns .abma<mb<mb %= mabmodmx=abrxx=qxm+rxqx

O caso não transbordante

No caso de não transbordar, poderíamos apenas calcular o módulo e concluir com ele. Em vez disso, o código calcula: Agora . Eu elidei uma preocupação aqui, no entanto. Pode ser que não se encaixe na mantissa da nossa variável de ponto flutuante. Enquanto se encaixar, no entanto, isso não será um problema, pois . Terminaremos com onde é algum erro de arredondamento cuja magnitude é menor que . Prosseguimos como antes:

y=xm=qx+rxm=qx+rxm=qx
xym=qxm+rxqxm=rxxmx=ab<m2x=x+eem
y=xm=qx+rx+em=qx+rx+em
neste caso, não podemos eliminar a porque, enquanto e , pode ser que ou , embora certamente esteja (estritamente) entre e , então é ou . Agorarx+emrx<me<mrx+e>mrx+e<0m2mrx+em0±1
xym=qxm+rxqxmrx+emm=rxrx+emm
A execução da operação do módulo agora se livrará desse termo extra, no entanto, devido à convenção que C escolher (-1)%m = -1,. Para obter uma convenção em que sempre retornamos um número positivo, podemos adicionar ao resultado, se negativo.m

O caso transbordante

Vamos assumir que estamos fazendo o inteiro aritmético mod , por exemplo, , e vamos assumir que significa . Agora a multiplicação vai terminar. Escreveremos para um número inteiro . Isso significa que o cálculo dará o número . O cálculo do ponto flutuante é como antes assumindo, novamente, que se encaixa na mantissa (o que pode exigir flutuações de precisão estendidas de 80 bits para maior ). Para o módulo, defina e entãoN264ab>Nm2>N>mx=ab=z+kNk<ma*bzmmz=qzm+rzkN=qNm+rNx=qzm+qNm+rz+rN. Como antes, armazenar como uma variável de ponto flutuante pode produzir algum erro de arredondamento , de novo, tenha . Se fizermos o mesmo cálculo que antes, parece: Eu tirei um coelho do chapéu aqui. Aqui, estamos adicionando o mod e o mod então . Como antes, modificamos por para obterx|e|<mx=x+e

zxmm=qzm+rzqzmqNmrz+rN+emm=rzqNmrz+rN+emm=rz+rNrz+rN+emm
NN qNm+rN=kN=0qNm=rNmrz+rN=xmodm , e, como antes, a convenção C pode levar a que isso seja negativo, o que precisará ser corrigido.

Há um possível problema restante. pode ser . Isso não causa nenhum problema, a menos que , nesse caso, você obtenha a resposta errada. Para isso significa . (Além disso, se não causa nenhum problema, pois terminaremos com ). Se configurarmos o hardware de ponto flutuante para arredondar para que , esse caso não será exibido. (Embora não esqueça a minha restrição de que a mantissa pode aguentar !).rz+rN+em22m>NN=264m>2632m=N0e0m

Conectando isso aos bits mais / menos significativos

Para abordar especificamente a parte que você citou, considere um número inteiro, , maior que 2, que consideraremos como base, como na "base ". No caso usual de , o número é representado como . Em uma representação de ponto flutuante, escreveríamos isso como . Agora, digamos que queremos multiplicar dois números (positivos) de um dígito base , o resultado exigiria no máximo dois dígitos, digamos onde (conforme exigido por uma representação [padrão] base ) e . Se estivermos restritos a apenas poder armazenar uma base,B10B=102121=2B+12.1×B1BcB+dB0c<B0d<BB dígito do resultado, existem duas opções óbvias, ou .cd

Escolher é equivalente a trabalhar com o mod como , é o que acontece com a aritmética inteira. (Aliás, no nível da montagem, a multiplicação inteira geralmente produz esses dois dígitos.)dBcB+d=dmodB

A aritmética de ponto flutuante, por outro lado, corresponde efetivamente à escolha , mas compensando isso incrementando o expoente. Com efeito, representamos o resultado como mas como podemos armazenar apenas um dígito base- , isso se torna apenas . (Na prática, consideraremos números como números de vários dígitos em uma base pequena (ou seja, 2), em vez de números de 1 ou 2 dígitos em uma base grande. Isso nos permite salvar alguns dos dígitos mais altos de se eles não são necessários para armazenar , mas no pior cenário de todos está perdido. Nenhum decc.d×B1Bc×B1dcdcé perdido até começarmos a ficar sem espaço no expoente. Para o código acima, isso não é um problema.)

Desde que possa ser representado fielmente no formato de ponto flutuante, a expressão pode ser vista como uma extração desse dígito superior na base . É possível ver o código e matemática acima como a interacção entre a base- e base- representações de um número.mabmmNm

Praticidades

Com base na seção 5.2.4.2.2 deste rascunho , o padrão C11 parece exigir apenas long doubleuma mantissa de aproximadamente 33 bits de comprimento. (Em particular, parece especificar apenas o número mínimo de dígitos decimais que podem ser fielmente representados.) Na prática, a maioria dos compiladores C ao direcionar-se a CPUs de uso geral e, principalmente, à família da família x86, usará os tipos IEEE754. Nesse caso, doubleterá efetivamente uma mantissa de 53 bits. As CPUs da família x86 suportam um formato de 80 bits com uma mantissa com efetivamente 64 bits, e vários, mas nem todos os compiladores long doubleindicam isso ao direcionar o x86. O intervalo de validade do código depende desses detalhes de implementação.

Derek Elkins deixou o SE
fonte
Excelente resposta! Um caso em que ainda estou coçando a cabeça: suponha que seja tão negativo que . Então o valor de será e, portanto, no intervalo . Suponha que esse valor seja . Em seguida, a conversão para (um tipo assinado) fará com que ela seja convertida em um número negativo. Por que o código faz a coisa certa neste caso? Em particular, torna-se após a conversão para (se fosse ) e, em seguida, a operação -e-make-positive deve produzir , em vez deerx+e<0a*b - c*mrx+mm..2m1263int64_trx+mrx+m264int64_t263%mrx264modmrx. (cont.)
DW
Ainda não encontrei um caso de teste que desencadeie esse comportamento. Em particular, não é possível encontrar nenhum valores para a, b, monde e em que o valor do é , isto é, onde e . Esse caso pode acontecer? Se puder, por que esse código fornece o resultado certo nessa situação? rx+e<0a*b - c*m263rx+e<0rx+m263
DW
Depois de tentar 1 bilhão de valores aleatórios, não consigo encontrar nenhum caso de teste que satisfaça essas condições. Então, talvez este caso seja impossível? Mas por que?
DW
Concordo que também não tenho certeza do que acontece nas extremidades. Neste caso, temos , e portanto o intervalo de é de a inclusive. Como isso não causa problemas nos casos , , . No entanto, não temos portanto, se podemos ter problemas, por exemplo, se para . Não sei se vai0rz<m0rN<m|e|<mrz+rN+em12m<N1012m<NmN/2m263N=264tem problemas; Eu acredito que sim. Por outro lado, se o modo de arredondamento estiver definido para arredondar para baixo, teremos e acho que funciona até . e0m=N1
Derek Elkins saiu de SE
Obrigado pela edição e pelo comentário. No entanto, ainda não vejo como isso resolve o caso de que estou falando. O caso que me preocupa é onde é e onde (para que o elenco produza um resultado negativo número). Seu comentário diz que não há problemas no caso , mas não entendo o porquê; é exatamente nesse caso que estou preocupado. Eu considerei os modos de arredondamento, mas, pelo que sei, parece que o modo de arredondamento padrão é "arredondado para o par", não "arredondado para baixo", de modo que isso não parece resolver o problema. rz+rN+em1rz+rN+m263int64_t1
DW