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 %= m
abmodmx=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
x−ym=qxm+rx−qxm=rxxmx=ab<m2x′=x+eemy=⌊x′m⌋=⌊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 . Agora
⌊rx+em⌋rx<me<mrx+e>mrx+e<0−m2m⌊rx+em⌋0±1x−ym=qxm+rx−qxm−⌊rx+em⌋m=rx−⌊rx+em⌋m
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*b
zmmz=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
z−⌊x′m⌋m=qzm+rz−qzm−qNm−⌊rz+rN+em⌋m=rz−qNm−⌊rz+rN+em⌋m=rz+rN−⌊rz+rN+em⌋m
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+em⌋22m>NN=264m>2632m=N0e≤0m
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+dB0≤c<B0≤d<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.m⌊abm⌋mNm
Praticidades
Com base na seção 5.2.4.2.2 deste rascunho , o padrão C11 parece exigir apenas long double
uma 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, double
terá 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 double
indicam isso ao direcionar o x86. O intervalo de validade do código depende desses detalhes de implementação.
a*b - c*m
int64_t
int64_t
%m
a
,b
,m
onde 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?a*b - c*m
int64_t