antiblock
Cyphriun
  • Chatbox

    Did you check out our Discord? https://discord.gg/FFdvMjk9xA
    You don't have permission to chat.
    Load More
Sign in to follow this  
V¡®u§

Delphi com assembly (Parte 6)

1 post in this topic

Aritmética Inteira de 128 bits (3)

Nessa terceira e última parte da série sobre inteiros de 128-bit (que

chamamos de inteiros gigantes, inteiros enormes ou inteiros largos)

vamos finalmente lidar com a aritimética propriamente dita- as quatro

operações fundamenteais (adição, subtração, multiplicação e divisão).

Antes de iniciar, gostaria de avisar que as rotinas introduzidas nas

duas partes anteriores foram corrigidas e também otimizadas. Ainda

não pude testá-las tanto quanto gostarias. Se você encontrar erros ou

tiver algum comentário sobre os fontes, por favor me envie um e-mail.

Adição

======

Como somamos dois números, cada um formado de quatro inteiros de 32-bit?

Bem, é bem fácil. Simplesmente somamos como faríamos com dois números de

quatro dígitos decimais (como 3597 e 0015, por exemplo), exceto que cada

dígito aqui pode ter aproximadamente 4 bilhões (2^32) de valores

distintos ao invés de apenas dez. O algoritmo é algo como:

function AddWithCarry(x: Longint; y: Longint;

var Carry: Boolean): Longint; forward;

function HugeAdd(x: Hugeint; y: Hugeint): Hugeint;

// Result := x + y;

var

Carry: Boolean;

begin

Carry := False;

Result[0] := AddWithCarry(x[0], y[0], Carry);

Result[1] := AddWithCarry(x[1], y[1], Carry);

Result[2] := AddWithCarry(x[2], y[2], Carry);

Result[3] := AddWithCarry(x[3], y[3], Carry);

end;

AddWithCarry é uma função fictícia que retorna um inteiro com os 32 bits

de mais baixa ordem do resultado da adição dos dois argumentos, mais um

se Carry (o terceiro argumento) for True. A rotina também altera o valor

de Carry com True o False dependendo da adição ter gerado um excesso ou

não (ou se o carry é 1 ou 0, se você prefere ver dessa forma). Na

prática, essa função não precisa ser fictícia:

function AddWithCarry(x: Longint; y: Longint;

var Carry: Boolean): integer;

asm

// if Carry then CF := 1 else CF := 0;

test byte ptr [ecx], -1 // efeito colateral: CF := 0;

jz @@NoCarry

stc // CF := 1;

@@NoCarry:

// Result := x + y + CF; CF := GeneratedCarry;

adc eax, edx

// Carry := CF;

setc byte ptr [ecx]

end;

é mais eficiente codificar HugeAdd inteiramente em assembler:

function HugeAdd(x: Hugeint; y: Hugeint): Hugeint;

// Result := x + y;

// Parameters: EAX = @x; EDX = @y; ECX = @Result

asm

push esi

mov esi, [eax+_0_] // ESI := x[0];

add esi, [edx+_0_] // ESI := ESI + y[0];

mov [ecx+_0_], esi // Result[0] := ESI;

mov esi, [eax+_1_] // ESI := x[1];

adc esi, [edx+_1_] // ESI := ESI + y[1] + Carry;

mov [ecx+_1_], esi // Result[1] := ESI;

mov esi, [eax+_2_] // ESI := x[2];

adc esi, [edx+_2_] // ESI := ESI + y[2] + Carry;

mov [ecx+_2_], esi // Result[2] := ESI;

mov esi, [eax+_3_] // ESI := x[3];

adc esi, [edx+_3_] // ESI := ESI + y[3] + Carry;

mov [ecx+_3_], esi // Result[3] := ESI;

pop esi

end;

Subtração

=========

A subtração acontece de forma semelhante à adição mas, ao invés de gerar

um excesso, a operação gera um "empréstimo" (também representado pelo

flag Carry) se o minuendo (primeiro operando) é menor que o subtraendo

(segundo operando):

function SubtractWithBorrow(x: Longint; y: Longint;

var Borrow: Boolean): Longint; forward;

function HugeSub(x: Hugeint; y: Hugeint): Hugeint;

// Result := x - y;

var

Borrow: Boolean;

begin

Borrow := False;

Result[0] := SubtractWithBorrow(x[0], y[0], Borrow);

Result[1] := SubtractWithBorrow(x[1], y[1], Borrow);

Result[2] := SubtractWithBorrow(x[2], y[2], Borrow);

Result[3] := SubtractWithBorrow(x[3], y[3], Borrow);

end;

function SubtractWithBorrow(x: Longint; y: Longint;

var Borrow: Boolean): Longint;

asm

// if Borrow then CF := 1 else CF := 0;

test byte ptr [ecx], -1 // Side-effect: CF := 0;

jz @@NoBorrow

stc // CF := 1;

@@NoBorrow:

// Result := x - y - CF; CF := NeededBorrow;

sbb eax, edx

// Borrow := CF;

setc byte ptr [ecx]

end;

Você deve estar pronto para escrever uma versão de HugeSub inteiramente

em assembler já que é análoga à HugeAdd; tudo que você deverá fazer é

substituir ADD e ADC com SUB e SBB respectivamente.

Número Oposto

=============

Dado um número, essas implementações de HugeNeg retornam seu oposto

(complemento de dois):

function HugeNeg(x: Hugeint): Hugeint;

begin

// Result := (Not x) + 1;

Result := HugeAdd(HugeNot(x), IntToHuge(1));

end;

function HugeNeg(x: Hugeint): Hugeint;

begin

// Result := 0 - x;

Result := HugeSub(IntToHuge(0), x);

end;

A segunda é mais simples e rápida pois involve uma operação apenas e, já

que sabemos subtrair, podemos implementá-la em assembler:

function HugeNeg(x: Hugeint): Hugeint;

// Result := -x;

// Parameters: EAX = @x; EDX = @Result

asm

// Result := 0 - x;

push esi

xor esi, esi

mov ecx, [eax+_0_] // x[0]

sub esi, ecx // 0 - x[0]

mov ecx, 0

mov [edx+_0_], esi // Result[0]

mov esi, [eax+_1_] // x[1]

sbb ecx, esi // 0 - x[1] - Borrow

mov esi, 0

mov [edx+_1_], ecx // Result[1]

mov ecx, [eax+_2_] // x[2]

sbb esi, ecx // 0 - x[2] - Borrow

mov ecx, 0

mov [edx+_2_], esi // Result[2]

mov esi, [eax+_3_] // x[3]

sbb ecx, esi // 0 - x[3] - Borrow

mov [edx+_3_], ecx // Result[3]

pop esi

end;

Multiplicação

=============

Uma forma de multiplicar números é através de um laço de adições:

function HugeMul(x: Hugeint; y: Hugeint): Hugeint;

begin

SetZero(Result);

while not HugeIsZero(y) do begin

Result := HugeAdd(Result, x);

HugeSub(y, 1)

end;

end;

Em termos computacionais, esse algoritmo é bastante pobre. Por exemplo,

se o valor de "y" for 4 milhões, o laço se repetiria 4 milhões de vezes!

De qualquer forma, a idéia ainda é boa se pudéssemos de alguma forma

acelerar o processo. Vamos então brincar um pouco com álgebra de bits:

x * y = x * (y[3]*2^96 + y[2]*2^64 + y[1]*2^32 + y[0]*2^0)

= (x*y[3])*2^96 + (x*y[2])*2^64 + (x*y[1])*2^32 + (x*y[0])*2^0

Agora nós reduzimos o problema de multiplicar dois inteiros gigantes

ao de multiplicar um inteiro gigante por um inteiro de 32-bit. Quando

multiplicamos o primeiro operando pelos quatro inteiros que compõem o

segundo operando, então deslocamos os resultados parciais por 0, 32,

64 e 96 bits (multiplicação por 2^0, 2^32, 2^64 e 2^96) e finalmente

somamos esses valores para obter o resultado final.

function HugeMulInt(x: Hugeint; y: Longint): Hugeint; forward;

function HugeMul(x: Hugeint; y: Hugeint): Hugeint;

begin

Result := HugeShl(HugeMulInt(x, y[3]), 96)

+ HugeShl(HugeMulInt(x, y[2]), 64)

+ HugeShl(HugeMulInt(x, y[1]), 32)

+ HugeMulInt(x, y[0]);

end;

Essa é a exata forma que multiplicamos números decimais quando usamos

papel, exceto que aqui a base é 2^32 ao invés de dez. Vejamos como

multiplicar um inteiro gigante por um inteiro:

function MultiplyWithCarry(x: Longint; y: Longint;

var Carry: Longint): Longint; forward;

function HugeMulInt(x: Hugeint; y: Longint): Hugeint;

// Result := x * y;

var

Carry: Longint;

begin

Carry := 0;

Result[0] := MultiplyWithCarry(x[0], y, Carry);

Result[1] := MultiplyWithCarry(x[1], y, Carry);

Result[2] := MultiplyWithCarry(x[2], y, Carry);

Result[3] := MultiplyWithCarry(x[3], y, Carry);

end;

function MultiplyWithCarry(x: Longint; y: Longint;

var Carry: Longint): integer;

// Result := LoDWord(x * y + Carry);

// Carry := HiDWord(x * y + Carry);

asm

// EDX:EAX := EAX * EDX; // x * y

mul edx

// Inc(EDX:EAX, Carry);

add eax, [ecx]

adc edx, 0

// Carry := EDX; // High order 32 bits of the result

mov [ecx], edx;

end;

MultiplyWithCarry é muito semelhante a AddWithCarry mas realiza uma

multiplicação ao invés de uma adição, e gera um excesso de 32-bit ao

invés de um bit apenas (a multiplicação de dois valores de 32-bit gera

um valor de 64-bit enquanto sua soma gera um valor de 33-bit).

MultiplyWithCarry primeiramente realiza a multiplicação sem sinal de

"x" (EAX) por "y" (EDX), usando o opcode MUL. O resultado de 64-bit

é um inteiro sem sinal contido em EDX:EAX, ao qual a função soma o

valor do parâmetro Carry. A função retorna os 32 bits mais baixos do

resultado final (localizado em EAX) e os 32 bits mais altos (EDX)

constituem o excesso para a próxima multiplicação, que é armazenado

no parâmetro Carry (passado por referência).

Uma implementação em assembler de HugeMul e HugeMulInt encontra-se no

código em anexo. Por razões de simplificação, nos exemplos acima, as

funções consideram que os números não possuem sinais, mas o código

anexo considera os sinais. O código de HugeMul também não chama

HugeMulInt ou HugeShl e está bastante otimizado.

Ao invés de considerar um inteiro gigante como quatro inteiros de

32-bit, podemos considerá-lo como 128 inteiros de 1-bit multiplicados

por 128 potências de 2:

bit127 * 2^127 + bit126 * 2^126 + ... + bit1 * 2^1 + bit0 * 2^0

Como cada bit só pode ser 0 ou 1, o algoritmo acima pode ser muito

simplificado:

function HugeMul(x: Hugeint; y: Hugeint): Hugeint;

// Result := x * y;

var

i: Longint;

begin

SetZero(Result);

for i := 0 to 127 do

if BitTest(y, i) then

Result := HugeAdd(Result, HugeShl(x, i));

end;

A idéia é somar diferentes potências de 2 de "x", dependendo essas

potências nos bits de "y". Por exemplo, se "y" for 20, os bits 5 e

3 estariam ligados (20 em decimal é 10100 em binário), de modo que

apenas duas adições são realizadas e o resultado é HugeShl(x, 3)

mais HugeShl(x, 5).

Esse algoritmo pode ser codificado de forma bastante eficiente em

assembler mas o primeiro algoritmo será ainda mais rápido. A razão de

mostrá-lo é que facilitará o entendimento do algoritmo que usaremos

para divisões.

Divisão

=======

Vamos primeiro ver o caso da divisão de um Hugeint por um inteiro de

32-bit, que deve ser fácil de entender:

function DivideWithRemainder(x: Longint; y: Longint;

var Remainder: Longint): Longint; forward;

function HugeDivInt(x: Hugeint; y: Longint): Hugeint;

// Result := x div y;

var

Remainder: Longint;

begin

Remainder := 0;

Result[0] := DivideWithRemainder(x[3], y, Remainder);

Result[1] := DivideWithRemainder(x[2], y, Remainder);

Result[2] := DivideWithRemainder(x[1], y, Remainder);

Result[3] := DivideWithRemainder(x[0], y, Remainder);

asm

mov edx, Remainder

end;

end;

function DivideWithRemainder(x: Longint; y: Longint;

var Remainder: Longint): Longint;

// Result := Remainder:x div y;

// Remainder := Remainder:x mod y;

asm

push esi

mov esi, edx // y

mov edx, [ecx] // Remainder

// EAX := EDX:EAX div ESI;

// EDX := EDX:EAX mod ESI;

div esi

// Remainder := EDX;

mov [ecx], edx;

pop esi

end;

HugeDivInt deixa o resto da divisão em EDX, de modo que possa ser usado

em uma função que retorne o resto da divisão:

function HugeModInt(dividend: Hugeint; divisor: Longint): Longint;

// Result := dividend mod divisor;

// Parameters: EAX = @dividend; EDX = @divisor;

asm

sub esp, TYPE(Hugeint) // Abrir espaço na pilha para um Hugeint

mov ecx, esp // com o resultado da divisão

call HugeDivInt // Realizar a divisão

add esp, TYPE(Hugeint) // Restaurar o ponteiro da pilha

mov eax, edx // Result := Remainder;

// o resto foi deixado em EDX

end;

Para o caso de dois inteiros gigantes, temos que pensar no algoritmo que

usaríamos para dividir um número de quatro dígitos decimais no papel.

Mas esse procedimento é complexo e lento demais pois envolve além das

divisões, multiplicações, subtrações, antecipação de etapas, etc. Há

alternativa possível para o algoritmo? Sim:

function HugeDiv(dividend: Hugeint; divisor: Hugeint): Hugeint;

// Result := dividend div divisor;

begin

if HugeIsZero(divisor) then

raise EDivByZero.CreateRes(@sDivByZero);

Result := 0;

while HugeCmp(dividend, divisor) >= 0 do begin

dividend := HugeSub(dividend, divisor);

Result := HugeAdd(Result, IntToHuge(1));

end;

end;

Claro, o algoritmo acaba sendo extremamente lento (se dividirmos

12 milhões por 3, o laço executará 4 milhões de vezes) mas podemos

acelerá-lo se subtrarimos do dividendo o divisor multiplicado por

diferentes potências de 2, da maior para a menor, definindo o bit

correspondente do resultado toda vez que efetuarmos a subtração (o

bit na posição da potência de 2 que foi usado). é o inverso do que

foi feito no caso da multiplicação acima. O processo de divisão seria

reduzido ao máximo de 128 subtrações.

No exemplo a seguir, o dividendo é 20 (10100 em binário) e o divisor

é 3 (11 em binário):

10100 - 11 * 2^2 = 10100 - 1100 = 1000 Result := 100

1000 - 11 * 2^1 = 1000 - 110 = 10 Result := 110

Inicialmente, 11 * 2^2 é o maior valor que é menor ou igual ao dividendo

então subtraímos esse valor do dividendo e definimos o bit 2 do

resultado pois subtraímos o divisor multiplicado por 2^2. Até o momento,

o resto da divisão é 8 (1000 em binário) e 11 * 2^1 é o maior valor que

é menor ou igual ao resto. Então subtraímos esse valor do resto e

definimos o bit 1 do resultado pois subtraímos o divisor multiplicado

por 2^1. O resto agora é 2 (10 em binátio) e como o divisor é maior que

esse valor, a divisão é interrompida. O resto da operação seria 2 (10 em

binário) e, como os bits 2 e 1 do resultado estão marcados, o resultado

é 110 em binário, ou 6 em notação decimal.

function HugeDiv(dividend: Hugeint; divisor: Hugeint): Hugeint;

var

_r_: Hugeint; // remainder

_d_: Hugeint; // divisor

_q_: Hugeint; // quotient

BitPosR, BitPosD, count: integer;

begin

_r_ := dividend;

_d_ := divisor;

HugeSetZero(_q_);

BitPosD := HugeBitScanReverse(_d_);

if BitPosD = -1 then RaiseDivByZero;

BitPosR := HugeBitScanReverse(_r_);

count := BitPosD - BitPosR;

if count > 0 then

_d_ := HugeShl(_d_, count);

repeat

if HugeCmp(_d_, _r_) <= 0 then begin

_r_ := HugeSub(_r_, _d_);

HugeBitSet(_q_, count);

end;

_d_ := HugeShr(_d_, 1);

dec(count);

until count < 0;

Result := _q_;

asm

lea edx, _r_

end;

end;

HugeBitScanReverse é a função que retorna a posição do primeiro bit não

zero, realizando a busca a partir do bit 127 até o bit 0. Se todos os

bits são zero, o resultado é -1. Usamos HugeBitScanReverse para

determinar a primeira potência de dois que devemos multiplicar o divisor

para iniciar a divisão.

A implementação em assembler de HugeDiv no código anexo suporta inteiros

com sinais. é apenas uma primeira aproximação que pode ainda ser muito

melhorada.

A função deixa em EDX o endereço do resto de modo que ele possa ser

usado por uma função que retorna o módulo da divisão:

function HugeMod(dividend: Hugeint; divisor: Hugeint): Hugeint;

// Result := dividend Mod divisor;

// Parameters: EAX = @dividend; EDX = @divisor; ECX = @Result

asm

push ecx // @Result

call HugeDiv // EDX := @remainder;

pop eax // EAX := @Result;

call HugeMov // EAX^ := EDX^;

end;

Share this post


Link to post
Share on other sites
antiblock
Rodnia | Alpha & Omega

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this