Implementação e desempenho dos números do BigInteger
Eu escrevi uma classe BigInteger em C ++ que deve poder executar operações em todos os números com qualquer tamanho. Atualmente, estou tentando obter um método de multiplicação muito rápido, comparando os algoritmos existentes e testando a quantidade de dígitos que eles funcionam melhor e encontrei resultados muito inesperados. Tentei fazer 20 multiplicações de 500 dígitos e cronometrei-as. Este foi o resultado:
karatsuba:
14.178 seconds
long multiplication:
0.879 seconds
Wikipedia me disse
Daqui resulta que, para n suficientemente grande, o algoritmo de Karatsuba executará menos mudanças e acréscimos de um dígito do que a multiplicação à mão, mesmo que sua etapa básica use mais adições e mudanças do que a fórmula direta. Para valores pequenos de n, no entanto, as operações extras de deslocamento e adição podem torná-lo mais lento que o método de mão longa. O ponto de retorno positivo depende da plataforma e do contexto do computador. Como regra geral, o Karatsuba geralmente é mais rápido quando os multiplicandos são maiores que 320–640 bits.
Como meus números têm pelo menos 1500 bits de comprimento, isso é bastante inesperado, pois a Wikipedia disse que o karatsuba deveria correr mais rápido. Acredito que meu problema possa estar no meu algoritmo de adição, mas não vejo como poderia torná-lo mais rápido porque ele já está sendo executado em O (n). Vou postar meu código abaixo para que você possa verificar minhas implementações. Vou deixar as partes irrelevantes fora disso.
Eu também estava pensando que talvez a estrutura que eu usei não fosse a melhor possível. Eu representei cada segmento de dados em little endian. Por exemplo, se eu tiver o número 123456789101112 armazenado em segmentos de dados de comprimento 3, ficaria assim:
{112.101.789.456.123}
é por isso que estou perguntando agora qual é a melhor estrutura e a melhor maneira de implementar uma classe BigInteger? E por que o algoritmo karatsuba é mais lento que o da multiplicação longa?
Este é o meu código: (desculpe pelo comprimento)
using namespace std;
bool __longmult=true;
bool __karatsuba=false;
struct BigInt {
public:
vector <int> digits;
BigInt(const char * number) {
//constructor is not relevant
}
BigInt() {}
void BigInt::operator = (BigInt a) {
digits=a.digits;
}
friend BigInt operator + (BigInt,BigInt);
friend BigInt operator * (BigInt,BigInt);
friend ostream& operator << (ostream&,BigInt);
};
BigInt operator + (BigInt a,BigInt b) {
if (b.digits.size()>a.digits.size()) {
a.digits.swap(b.digits); //make sure a has more or equal amount of digits than b
}
int carry=0;
for (unsigned int i=0;i<a.digits.size();i++) {
int sum;
if (i<b.digits.size()) {
sum=b.digits[i]+a.digits[i]+carry;
} else if (carry==1) {
sum=a.digits[i]+carry;
} else {
break; // if carry is 0 and no more digits in b are left then we are done already
}
if (sum>=1000000000) {
a.digits[i]=sum-1000000000;
carry=1;
} else {
a.digits[i]=sum;
carry=0;
}
}
if (carry) {
a.digits.push_back(1);
}
return a;
}
BigInt operator * (BigInt a,BigInt b) {
if (__longmult) {
BigInt res;
for (unsigned int i=0;i<b.digits.size();i++) {
BigInt temp;
temp.digits.insert(temp.digits.end(),i,0); //shift to left for i 'digits'
int carry=0;
for (unsigned int j=0;j<a.digits.size();j++) {
long long prod=b.digits[i];
prod*=a.digits[j];
prod+=carry;
int t=prod%1000000000;
temp.digits.push_back(t);
carry=(prod-t)/1000000000;
}
if (carry>0) {
temp.digits.push_back(carry);
}
res+=temp;
}
return res;
} else if (__karatsuba) {
BigInt res;
BigInt a1,a0,b1,b0;
assert(a.digits.size()>0 && b.digits.size()>0);
while (a.digits.size()!=b.digits.size()) { //add zeroes for equal size
if (a.digits.size()>b.digits.size()) {
b.digits.push_back(0);
} else {
a.digits.push_back(0);
}
}
if (a.digits.size()==1) {
long long prod=a.digits[0];
prod*=b.digits[0];
res=prod;//conversion from long long to BigInt runs in constant time
return res;
} else {
for (unsigned int i=0;i<a.digits.size();i++) {
if (i<(a.digits.size()+(a.digits.size()&1))/2) { //split the number in 2 equal parts
a0.digits.push_back(a.digits[i]);
b0.digits.push_back(b.digits[i]);
} else {
a1.digits.push_back(a.digits[i]);
b1.digits.push_back(b.digits[i]);
}
}
}
BigInt z2=a1*b1;
BigInt z0=a0*b0;
BigInt z1 = (a1 + a0)*(b1 + b0) - z2 - z0;
if (z2==0 && z1==0) {
res=z0;
} else if (z2==0) {
z1.digits.insert(z1.digits.begin(),a0.digits.size(),0);
res=z1+z0;
} else {
z1.digits.insert(z1.digits.begin(),a0.digits.size(),0);
z2.digits.insert(z2.digits.begin(),2*a0.digits.size(),0);
res=z2+z1+z0;
}
return res;
}
}
int main() {
clock_t start, end;
BigInt a("984561231354629875468546546534125215534125215634987498548489456125215421563498749854848945612385663498749854848945612521542156349874985484894561238561698774565123165221393856169877456512316552156349874985484894561238561698774565123165221392213935215634987498548489456123856169877456512316522139521563498749854848945612385616987745651231652213949651465123151354686324848945612385616987745651231652213949651465123151354684132319321005482265341252156349874985484894561252154215634987498548489456123856264596162131");
BigInt b("453412521563498749853412521563498749854848945612521542156349874985484894561238565484894561252154215634987498548489456123856848945612385616935462987546854521563498749854848945653412521563498749854848945612521542156349874985484894561238561238754579785616987745651231652213965465341235215634987495215634987498548489456123856169877456512316522139854848774565123165223546298754685465465341235215634987498548354629875468546546534123521563498749854844139496514651231513546298754685465465341235215634987498548435468");
__longmult=false;
__karatsuba=true;
start=clock();
for (int i=0;i<20;i++) {
a*b;
}
end=clock();
printf("\nTook %f seconds\n", (double)(end-start)/CLOCKS_PER_SEC);
__longmult=true;
__karatsuba=false;
start=clock();
for (int i=0;i<20;i++) {
a*b;
}
end=clock();
printf("\nTook %f seconds\n", (double)(end-start)/CLOCKS_PER_SEC);
return 0;
}