Um meine Bignum-Divisons zu beschleunigen, muss ich den Betrieb beschleunigen y = x^2
für Bigints, die als dynamische Arrays von DWORDs ohne Vorzeichen dargestellt werden. Deutlich sein:
DWORD x[n+1] = { LSW, ......, MSW };
- wobei n+1 die Anzahl der verwendeten DWORDs ist
- also Wert der Zahl
x = x[0]+x[1]<<32 + ... x[N]<<32*(n)
Die Frage ist: Wie rechne ich y = x^2
so schnell wie möglich ohne Präzisionsverlust?
– Verwenden C++ und mit ganzzahliger Arithmetik (32bit mit Carry) zur Verfügung.
Mein aktueller Ansatz ist die Anwendung der Multiplikation y = x*x
und vermeiden Sie mehrfache Multiplikationen.
Zum Beispiel:
x = x[0] + x[1]<<32 + ... x[n]<<32*(n)
Der Einfachheit halber schreibe ich es um:
x = x0+ x1 + x2 + ... + xn
wobei index die Adresse innerhalb des Arrays darstellt, also:
y = x*x
y = (x0 + x1 + x2 + ...xn)*(x0 + x1 + x2 + ...xn)
y = x0*(x0 + x1 + x2 + ...xn) + x1*(x0 + x1 + x2 + ...xn) + x2*(x0 + x1 + x2 + ...xn) + ...xn*(x0 + x1 + x2 + ...xn)
y0 = x0*x0
y1 = x1*x0 + x0*x1
y2 = x2*x0 + x1*x1 + x0*x2
y3 = x3*x0 + x2*x1 + x1*x2
...
y(2n-3) = xn(n-2)*x(n ) + x(n-1)*x(n-1) + x(n )*x(n-2)
y(2n-2) = xn(n-1)*x(n ) + x(n )*x(n-1)
y(2n-1) = xn(n )*x(n )
Nach genauerem Hinsehen ist klar, dass fast alle xi*xj
erscheint zweimal (nicht das erste und letzte), was bedeutet, dass N*N
Multiplikationen können durch ersetzt werden (N+1)*(N/2)
Multiplikationen. PS 32bit*32bit = 64bit
so das Ergebnis von jedem mul+add
Betrieb wird behandelt als 64+1 bit
.
Gibt es einen besseren Weg, dies schnell zu berechnen? Alles, was ich bei der Suche gefunden habe, waren sqrts-Algorithmen, nicht sqr …
Schnell quadrat
!!! Beachten Sie, dass alle Zahlen in meinem Code zuerst MSW sind, … nicht wie im obigen Test (es gibt zuerst LSW, um die Gleichungen zu vereinfachen, sonst wäre es ein Index-Chaos).
Aktuelle funktionale fsqr-Implementierung
void arbnum::sqr(const arbnum &x)
{
// O((N+1)*N/2)
arbnum c;
DWORD h, l;
int N, nx, nc, i, i0, i1, k;
c._alloc(x.siz + x.siz + 1);
nx = x.siz - 1;
nc = c.siz - 1;
N = nx + nx;
for (i=0; i<=nc; i++)
c.dat[i]=0;
for (i=1; i<N; i++)
for (i0=0; (i0<=nx) && (i0<=i); i0++)
{
i1 = i - i0;
if (i0 >= i1)
break;
if (i1 > nx)
continue;
h = x.dat[nx-i0];
if (!h)
continue;
l = x.dat[nx-i1];
if (!l)
continue;
alu.mul(h, l, h, l);
k = nc - i;
if (k >= 0)
alu.add(c.dat[k], c.dat[k], l);
k--;
if (k>=0)
alu.adc(c.dat[k], c.dat[k],h);
k--;
for (; (alu.cy) && (k>=0); k--)
alu.inc(c.dat[k]);
}
c.shl(1);
for (i = 0; i <= N; i += 2)
{
i0 = i>>1;
h = x.dat[nx-i0];
if (!h)
continue;
alu.mul(h, l, h, h);
k = nc - i;
if (k >= 0)
alu.add(c.dat[k], c.dat[k],l);
k--;
if (k>=0)
alu.adc(c.dat[k], c.dat[k], h);
k--;
for (; (alu.cy) && (k >= 0); k--)
alu.inc(c.dat[k]);
}
c.bits = c.siz<<5;
c.exp = x.exp + x.exp + ((c.siz - x.siz - x.siz)<<5) + 1;
c.sig = sig;
*this = c;
}
Verwendung der Karatsuba-Multiplikation
(dank Calpis)
Ich habe die Karatsuba-Multiplikation implementiert, aber die Ergebnisse sind sogar massiv langsamer als bei der Verwendung von simple O(N^2)
Multiplikation, wahrscheinlich wegen dieser schrecklichen Rekursion, die ich nicht vermeiden kann. Der Kompromiss muss bei wirklich großen Zahlen sein (größer als Hunderte von Ziffern) … aber selbst dann gibt es viele Speicherübertragungen. Gibt es eine Möglichkeit, Rekursionsaufrufe zu vermeiden (nicht rekursive Variante, … Fast alle rekursiven Algorithmen können auf diese Weise ausgeführt werden). Trotzdem werde ich versuchen, die Dinge zu optimieren und zu sehen, was passiert (Normalisierungen usw. vermeiden, es könnte auch ein dummer Fehler im Code sein). Wie auch immer, nachdem ich Karatsuba für den Fall gelöst habe x*x
es gibt nicht viel leistungsgewinn.
Optimierte Karatsuba-Multiplikation
Leistungstest für y = x^2 looped 1000x times, 0.9 < x < 1 ~ 32*98 bits
:
x = 0.98765588997654321000000009876... | 98*32 bits
sqr [ 213.989 ms ] ... O((N+1)*N/2) fast sqr
mul1[ 363.472 ms ] ... O(N^2) classic multiplication
mul2[ 349.384 ms ] ... O(3*(N^log2(3))) optimized Karatsuba multiplication
mul3[ 9345.127 ms] ... O(3*(N^log2(3))) unoptimized Karatsuba multiplication
x = 0.98765588997654321000... | 195*32 bits
sqr [ 883.01 ms ]
mul1[ 1427.02 ms ]
mul2[ 1089.84 ms ]
x = 0.98765588997654321000... | 389*32 bits
sqr [ 3189.19 ms ]
mul1[ 5553.23 ms ]
mul2[ 3159.07 ms ]
Nach Optimierungen für Karatsuba ist der Code massiv schneller als zuvor. Für kleinere Zahlen ist es jedoch etwas weniger als die Hälfte meiner Geschwindigkeit O(N^2)
Multiplikation. Für größere Zahlen ist es schneller mit dem Verhältnis, das durch die Komplexität von Booth-Multiplikationen gegeben ist. Der Schwellenwert für die Multiplikation liegt bei etwa 32 * 98 Bit und für sqr bei etwa 32 * 389 Bit. Wenn also die Summe der Eingabebits diesen Schwellenwert überschreitet, wird die Karatsuba-Multiplikation zur Beschleunigung der Multiplikation verwendet, und das gilt auch für sqr.
Übrigens, Optimierungen enthalten:
- Heap-Trashing durch zu großes Rekursionsargument minimieren
- Vermeidung jeglicher Bignum-Arithmetik (+,-) 32-Bit-ALU mit Übertrag wird stattdessen verwendet.
- Ignorieren
0*y
oderx*0
oder0*0
Fälle - Eingabe neu formatieren
x,y
Zahlengrößen in Zweierpotenzen, um eine Neuzuweisung zu vermeiden - Implementieren Sie die Modulo-Multiplikation für
z1 = (x0 + x1)*(y0 + y1)
Rekursion zu minimieren
Modifizierte Schönhage-Strassen-Multiplikation zur sqr-Implementierung
Ich habe die Verwendung von getestet FFT und NTT transformiert, um die sqr-Berechnung zu beschleunigen. Die Ergebnisse sind diese:
-
FFT
Verlieren die Genauigkeit und benötigen daher hochpräzise komplexe Zahlen. Dies verlangsamt die Dinge tatsächlich erheblich, sodass keine Beschleunigung vorhanden ist. Das Ergebnis ist also nicht genau (kann falsch gerundet werden). FFT ist (vorerst) unbrauchbar
-
NTT
NTT ist endliches Feld DFT und somit tritt kein Genauigkeitsverlust auf. Es braucht modulare Arithmetik für vorzeichenlose Ganzzahlen:
modpow, modmul, modadd
undmodsub
.ich benutze
DWORD
(vorzeichenlose 32-Bit-Ganzzahlen). Die NTT Die Größe des Ein-/Ausgangsvektors ist aufgrund von Überlaufproblemen begrenzt!!! Für modulare 32-Bit-Arithmetik gilt:N
ist auf … begrenzt(2^32)/(max(input[])^2)
damitbigint
muss in kleinere Stücke geteilt werden (ich verwendeBYTES
also maximale Größe vonbigint
verarbeitet ist(2^32)/((2^8)^2) = 2^16 bytes = 2^14 DWORDs = 16384 DWORDs)
Die
sqr
verwendet nur1xNTT + 1xINTT
anstatt2xNTT + 1xINTT
zur Multiplikation aber NTT Die Verwendung ist zu langsam und die Größe der Schwellenzahl ist zu groß für die praktische Verwendung in meiner Implementierung (zmul
und auch fürsqr
).Es ist möglich, dass dies sogar über der Überlaufgrenze liegt, daher sollte 64-Bit-Modularithmetik verwendet werden, was die Dinge noch mehr verlangsamen kann. Damit NTT ist für meine zwecke auch unbrauchbar.
Einige Messungen:
a = 0.98765588997654321000 | 389*32 bits
looped 1x times
sqr1[ 3.177 ms ] fast sqr
sqr2[ 720.419 ms ] NTT sqr
mul1[ 5.588 ms ] simpe mul
mul2[ 3.172 ms ] karatsuba mul
mul3[ 1053.382 ms ] NTT mul
Meine Umsetzung:
void arbnum::sqr_NTT(const arbnum &x)
{
// O(N*log(N)*(log(log(N)))) - 1x NTT
// Schönhage-Strassen sqr
// To prevent NTT overflow: n <= 48K * 8 bit -> result siz <= 12K * 32 bit -> x.siz + y.siz <= 12K!!!
int i, j, k, n;
int s = x.sig*x.sig, exp0 = x.exp + x.exp - ((x.siz+x.siz)<<5) + 2;
i = x.siz;
for (n = 1; n < i; n<<=1)
;
if (n + n > 0x3000) {
_error(_arbnum_error_TooBigNumber);
zero();
return;
}
n <<= 3;
DWORD *xx, *yy, q, qq;
xx = new DWORD[n+n];
#ifdef _mmap_h
if (xx)
mmap_new(xx, (n+n) << 2);
#endif
if (xx==NULL) {
_error(_arbnum_error_NotEnoughMemory);
zero();
return;
}
yy = xx + n;
// Zero padding (and split DWORDs to BYTEs)
for (i--, k=0; i >= 0; i--)
{
q = x.dat[i];
xx[k] = q&0xFF; k++; q>>=8;
xx[k] = q&0xFF; k++; q>>=8;
xx[k] = q&0xFF; k++; q>>=8;
xx[k] = q&0xFF; k++;
}
for (;k<n;k++)
xx[k] = 0;
//NTT
fourier_NTT ntt;
ntt.NTT(yy,xx,n); // init NTT for n
// Convolution
for (i=0; i<n; i++)
yy[i] = modmul(yy[i], yy[i], ntt.p);
//INTT
ntt.INTT(xx, yy);
//suma
q=0;
for (i = 0, j = 0; i<n; i++) {
qq = xx[i];
q += qq&0xFF;
yy[n-i-1] = q&0xFF;
q>>=8;
qq>>=8;
q+=qq;
}
// Merge WORDs to DWORDs and copy them to result
_alloc(n>>2);
for (i = 0, j = 0; i<siz; i++)
{
q =(yy[j]<<24)&0xFF000000; j++;
q |=(yy[j]<<16)&0x00FF0000; j++;
q |=(yy[j]<< 8)&0x0000FF00; j++;
q |=(yy[j] )&0x000000FF; j++;
dat[i] = q;
}
#ifdef _mmap_h
if (xx)
mmap_del(xx);
#endif
delete xx;
bits = siz<<5;
sig = s;
exp = exp0 + (siz<<5) - 1;
// _normalize();
}
Fazit
Für kleinere Zahlen ist es die beste Option für mein schnelles sqr
Ansatz und nach Schwelle Karatsuba multiplizieren ist besser. Aber ich denke immer noch, dass es etwas Triviales geben sollte, das wir übersehen haben. Hat jemand andere Ideen?
NTT-Optimierung
Nach massiv intensiven Optimierungen (meistens NTT): Stapelüberlauffrage Modulare Arithmetik und NTT-Optimierungen (Finite Field DFT).
Einige Werte haben sich geändert:
a = 0.98765588997654321000 | 1553*32bits
looped 10x times
mul2[ 28.585 ms ] Karatsuba mul
mul3[ 26.311 ms ] NTT mul
Also jetzt NTT Multiplikation ist schließlich schneller als Karatsuba nach etwa 1500 * 32-Bit-Schwelle.
Einige Messungen und Fehler entdeckt
a = 0.99991970486 | 1553*32 bits
looped: 10x
sqr1[ 58.656 ms ] fast sqr
sqr2[ 13.447 ms ] NTT sqr
mul1[ 102.563 ms ] simpe mul
mul2[ 28.916 ms ] Karatsuba mul Error
mul3[ 19.470 ms ] NTT mul
Ich habe herausgefunden, dass meine Karatsuba (über/unter)fließen die LSB von jedem DWORD
Abschnitt von bignum. Wenn ich recherchiert habe, werde ich den Code aktualisieren …
Auch nach weiter NTT Optimierungen die Schwellenwerte verändert, so z NTT Quadrat es ist 310*32 bits = 9920 bits
von Operandund für NTT-Mul es ist 1396*32 bits = 44672 bits
von Ergebnis (Summe der Bits von Operanden).
Karatsuba-Code repariert dank @greybeard
//---------------------------------------------------------------------------
void arbnum::_mul_karatsuba(DWORD *z, DWORD *x, DWORD *y, int n)
{
// Recursion for Karatsuba
// z[2n] = x[n]*y[n];
// n=2^m
int i;
for (i=0; i<n; i++)
if (x[i]) {
i=-1;
break;
} // x==0 ?
if (i < 0)
for (i = 0; i<n; i++)
if (y[i]) {
i = -1;
break;
} // y==0 ?
if (i >= 0) {
for (i = 0; i < n + n; i++)
z[i]=0;
return;
} // 0.? = 0
if (n == 1) {
alu.mul(z[0], z[1], x[0], y[0]);
return;
}
if (n< 1)
return;
int n2 = n>>1;
_mul_karatsuba(z+n, x+n2, y+n2, n2); // z0 = x0.y0
_mul_karatsuba(z , x , y , n2); // z2 = x1.y1
DWORD *q = new DWORD[n<<1], *q0, *q1, *qq;
BYTE cx,cy;
if (q == NULL) {
_error(_arbnum_error_NotEnoughMemory);
return;
}
#define _add { alu.add(qq[i], q0[i], q1[i]); for (i--; i>=0; i--) alu.adc(qq[i], q0[i], q1[i]); } // qq = q0 + q1 ...[i..0]
#define _sub { alu.sub(qq[i], q0[i], q1[i]); for (i--; i>=0; i--) alu.sbc(qq[i], q0[i], q1[i]); } // qq = q0 - q1 ...[i..0]
qq = q;
q0 = x + n2;
q1 = x;
i = n2 - 1;
_add;
cx = alu.cy; // =x0+x1
qq = q + n2;
q0 = y + n2;
q1 = y;
i = n2 - 1;
_add;
cy = alu.cy; // =y0+y1
_mul_karatsuba(q + n, q + n2, q, n2); // =(x0+x1)(y0+y1) mod ((2^N)-1)
if (cx) {
qq = q + n;
q0 = qq;
q1 = q + n2;
i = n2 - 1;
_add;
cx = alu.cy;
}// += cx*(y0 + y1) << n2
if (cy) {
qq = q + n;
q0 = qq;
q1 = q;
i = n2 -1;
_add;
cy = alu.cy;
}// +=cy*(x0+x1)<<n2
qq = q + n; q0 = qq; q1 = z + n; i = n - 1; _sub; // -=z0
qq = q + n; q0 = qq; q1 = z; i = n - 1; _sub; // -=z2
qq = z + n2; q0 = qq; q1 = q + n; i = n - 1; _add; // z1=(x0+x1)(y0+y1)-z0-z2
DWORD ccc=0;
if (alu.cy)
ccc++; // Handle carry from last operation
if (cx || cy)
ccc++; // Handle carry from before last operation
if (ccc)
{
i = n2 - 1;
alu.add(z[i], z[i], ccc);
for (i--; i>=0; i--)
if (alu.cy)
alu.inc(z[i]);
else
break;
}
delete[] q;
#undef _add
#undef _sub
}
//---------------------------------------------------------------------------
void arbnum::mul_karatsuba(const arbnum &x, const arbnum &y)
{
// O(3*(N)^log2(3)) ~ O(3*(N^1.585))
// Karatsuba multiplication
//
int s = x.sig*y.sig;
arbnum a, b;
a = x;
b = y;
a.sig = +1;
b.sig = +1;
int i, n;
for (n = 1; (n < a.siz) || (n < b.siz); n <<= 1)
;
a._realloc(n);
b._realloc(n);
_alloc(n + n);
for (i=0; i < siz; i++)
dat[i]=0;
_mul_karatsuba(dat, a.dat, b.dat, n);
bits = siz << 5;
sig = s;
exp = a.exp + b.exp + ((siz-a.siz-b.siz)<<5) + 1;
// _normalize();
}
//---------------------------------------------------------------------------
Mein arbnum
Zahlendarstellung:
// dat is MSDW first ... LSDW last
DWORD *dat; int siz,exp,sig,bits;
dat[siz]
ist die Gottesanbeterin. LSDW bedeutet niedrigstwertiges DWORD.exp
ist der Exponent von MSB vondat[0]
-
Das erste Nicht-Null-Bit ist in der Mantisse vorhanden!!!
// |-----|---------------------------|---------------|------| // | sig | MSB mantisa LSB | exponent | bits | // |-----|---------------------------|---------------|------| // | +1 | 0.(0 ... 0) | 2^0 | 0 | +zero // | -1 | 0.(0 ... 0) | 2^0 | 0 | -zero // |-----|---------------------------|---------------|------| // | +1 | 1.(dat[0] ... dat[siz-1]) | 2^exp | n | +number // | -1 | 1.(dat[0] ... dat[siz-1]) | 2^exp | n | -number // |-----|---------------------------|---------------|------| // | +1 | 1.0 | 2^+0x7FFFFFFE | 1 | +infinity // | -1 | 1.0 | 2^+0x7FFFFFFE | 1 | -infinity // |-----|---------------------------|---------------|------|
Meine Frage ist, warum Sie sich entschieden haben, Ihre eigene bignum-Implementierung zu implementieren? Die GNU Multiple Precision Arithmetic Library ist wahrscheinlich eine der am häufigsten verwendeten bignum-Bibliotheken und sollte mit all ihren Operationen ziemlich optimal sein.
– Irgendein Programmierer-Typ
27. August 2013 um 12:27 Uhr
Ich verwende aus Kompatibilitätsgründen meine eigenen Bignum-Bibliotheken. Das Portieren des gesamten Codes in verschiedene Bibliotheken ist zeitaufwändiger, als es auf den ersten Blick erscheinen mag (und manchmal aufgrund von Compiler-Inkompatibilitäten, insbesondere mit gcc-Code, nicht einmal möglich). Bin gerade am tüfteln,… läuft alles wie es soll aber mehr Speed ist immer erwünscht 🙂
– Spektre
27. August 2013 um 12:43 Uhr
PS für die Verwendung von NTT empfehle ich dringend, dass NTT mit 4x höherer Genauigkeit als Eingabewerte berechnet wird (also müssen Sie 8-Bit-Zahlen in 32-Bit-Zahlen umwandeln), um den Kompromiss zwischen maximaler Array-Größe und Geschwindigkeit zu finden
– Spektre
2. September 2013 um 14:56 Uhr