Divisione tra big int

di il
7 risposte

Divisione tra big int

Ciao, mi sto cimentando nell'implementazione delle 4 operazioni tra big int.

Tempo fa lo avevo già fatto utilizzando come contenitori per i big int delle stringhe e come algoritmi per le operazioni quelli classici insegnati a scuola (con alcune piccole modifiche).
Per migliorare le prestazioni, nella nuova libreria che sto scrivendo, ho pensato di considerare più cifre alla volta e quindi ho utilizzato come contenitore dei big int un vector<uint32_t>: in pratica partendo da destra divido il big int in gruppi di nove cifre che saranno contenute all'interno di un uint32_t. Il motivo per cui utilizzo interi senza segno a 32 bit è che poi posso effettuare il prodotto (e quindi anche la somma e la differenza) tra due uint32_t utilizzando un uint64_t .
Quindi, a differenza della versione con le stringhe, dove effettuo i calcoli in base 10, qui effettuo i calcoli in base 10^9.
Poi, per migliorare ulteriormente le prestazioni, ho deciso di effettuare i calcoli non in base 10^9, ma in base 2^32, in modo da poter sfruttare la maggior efficienza di n>>32 (dove n è un uint64_t) rispetto a n/10^9 e di (uint32_t)n rispetto a n%10^9.

Relativamente a +, - e * ho riscontrato un netto miglioramento delle prestazioni (per la moltiplicazione si potrebbero anche usare algoritmi come quello di Karatsuba , ma per per il momento mi accontento del codice che ho già scritto). Per la divisione intera, invece, la situazione mi sembra un po' più complicata... cerco di fare il punto della situazione:

- l'algoritmo di divisione che sto utilizzando, tralasciando le varie versioni implementate e le varie ottimizzazioni (si spera) adottate, è quello della classica divisione in colonna che viene insegnata alle elementari;

- le singole cifre q_i del quoziente Q, a seconda della base b adottata, varieranno nell'intervallo [0;b-1]. Una volta assicuratoci con un semplice confronto che q_i!=0, possiamo facilmente fissare q_i_MAX, ossia il massimo valore che quella determinata cifra del quoziente può assumere. Fissato q_i_MAX, per giungere al q_i effettivo bisognerà ovviamente fare dei "tentativi", e il punto è proprio questo, ossia: maggiore è la base b adottata, maggiori saranno in media i tentativi da fare per giungere al valore di q_i corretto;

- partendo dalla suddetta osservazione, ho pensato che la cosa migliore sarebbe stata quella di eliminare i "tentativi" effettuando la divisione in base 2, dove infatti q_i, una volta escluso lo 0 con un semplice confronto, non può che essere 1.

Siete d'accordo, almeno in linea di massima, con questo mio ragionamento?

Per effettuare la divisione in base 2 ho convertito il big int da vector<uint32_t> in un vettore di valori booleani (inizialmente ho utilizzato un vector<bool>, ma poi l'ho sostituito con un vector<uint16_t> che, malgrado il maggior consumo di memoria, presenta tempi di accesso nettamente inferiori).
Alla fine della fiera però, facendo un test su una divisione tra un numero a 8400 cifre ed uno a 1100 cifre, la vecchia versione con le stringhe in base 10 impiega 170ms, mentre quella nuova in base 2 circa 320ms (ovviamente misurando solo l'esecuzione delle funzioni di divisione).

Di seguito il codice, e la spiegazione dell'algoritmo utilizzato, relativo alle due versioni:

- versione con le stringhe in base 10:
#include <iostream>
#include <algorithm>
#include <chrono>
#include <string>

using namespace std;
using namespace std::chrono;

string addition(const string &s1, const string &s2)
{
    string s3;
    unsigned int n = 0;
    for(unsigned int i = 0; i < s1.size() || i < s2.size() || n; ++i)
    {
        if(i < s1.size())
        {
            n += s1[s1.size() - 1 - i] - '0';
        }
        if(i < s2.size())
        {
            n += s2[s2.size() - 1 - i] - '0';
        }
        s3.push_back(n % 10 + '0');
        n /= 10;
    }
    reverse(s3.begin(), s3.end());
    return s3;
}

string subtraction(const string &s1, const string &s2)
{
    string s3;
    unsigned int n = 10;
    for(unsigned int i = 0; i < s1.size(); ++i)
    {
        n = 10 + s1[s1.size() - 1 - i] - '0' - (n < 10);
        if(i < s2.size())
        {
            n -= s2[s2.size() - 1 - i] - '0';
        }
        s3.push_back(n % 10 + '0');
    }
    for(unsigned int i = s3.size() - 1; i && s3[i] == '0'; --i)
    {
        s3.pop_back();
    }
    reverse(s3.begin(), s3.end());
    return s3;
}

string multiplication(const string &s1, const string &s2)
{
    string s3 = "0";
    string temp;
    for(unsigned int i = 0; i < s1.size(); ++i)
    {
        temp.resize(0);
        unsigned int n = 0;
        for(unsigned int j = 0; j < s2.size(); ++j)
        {
            n = (s1[s1.size() - 1 - i] - '0') * (s2[s2.size() - 1 - j] - '0') + n / 10;
            temp.push_back(n % 10 + '0');
        }
        if(n > 9)
        {
            temp.push_back(n / 10 + '0');
        }
        reverse(temp.begin(), temp.end());
        for(unsigned int j = 0; j < i; ++j)
        {
            temp.push_back('0');
        }
        s3 = addition(s3, temp);
    }
    return s3;
}

int compare_abs(const string &s1, const string &s2)
{
    if(s1.size() != s2.size())
    {
        return((int)s1.size() - s2.size());
    }
    unsigned int i;
    for(i = 0; i < s1.size() - 1 && s1[i] == s2[i]; ++i);
    return((int)s1[i] - s2[i]);
}

string division(const string &s1, const string &s2, const bool flag_quotient)
{
    string quotient;
    string rest;
    unsigned int i;
    for(i = 0; compare_abs(rest, s2) < 0; ++i)
    {
        rest.push_back(s1[i]);
    }
    while(true)
    {
        unsigned int q;
        int relation = compare_abs(rest, s2);
        if(relation <= 0)
        {
            q = !relation;
            if(!relation)
            {
                rest = "0";
            }
        }
        else
        {
            unsigned int r = rest[0] - '0';
            if(rest.size() > s2.size())
            {
                r = r * 10 + rest[1] - '0';
            }
            q = r / (s2[0] - '0');
            if(q > 9)
            {
                q = 9;
            }
            while(q > 1)
            {
                bool flag = true;
                unsigned int a = r;
                for(unsigned int j = 0; j < s2.size(); ++j)
                {
                    unsigned int b = q * (s2[j] - '0');
                    if(a < b)
                    {
                        --q;
                        flag = false;
                        break;
                    }
                    if(a - b >= q)
                    {
                        break;
                    }
                    if(j != s2.size() - 1)
                    {
                        a = (a - b) * 10 + rest[j + 1 + (rest.size() > s2.size())] - '0';
                    }
                }
                if(flag)
                {
                    break;
                }
            }
            rest = subtraction(rest, multiplication({char(q + '0')}, s2));
        }
        quotient.push_back(q + '0');
        if(i == s1.size())
        {
            break;
        }
        if(rest[0] == '0')
        {
            rest.resize(0);
        }
        rest.push_back(s1[i++]);
    }
    return(flag_quotient ? quotient : rest);
}

int main()
{
    string a("123456789987654321");
    string b("157988");
    
    auto start1 = high_resolution_clock::now();
    string q = division(a, b, true);
    auto stop1 = high_resolution_clock::now();
    auto duration1 = duration_cast<milliseconds>(stop1 - start1);

    string r = division(a, b, false);

    cout << q << endl << endl;
    cout << r << endl << endl;
    cout << duration1.count() << " ms" << endl;
}
- versione con il vettore di booleani in base 2:
#include <iostream>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>

using namespace std;
using namespace std::chrono;

const uint8_t N = 32;
const uint32_t S_10 = 1000000000;

vector<uint32_t> from_2_to_10(vector<uint32_t> v_2)
{
    vector<uint32_t> v_10;
    do
    {
        uint64_t r = 0;
        unsigned int i = 0;
        for(unsigned int j = 0; j < v_2.size(); ++j)
        {
            r = (r << N) + v_2[j];
            if(i || r / S_10)
            {
                v_2[i++] = r / S_10;
                r %= S_10;
            }
        }
        v_2.resize(i);
        v_10.push_back(r);
    }
    while(v_2.size());
    return v_10;
}

vector<uint32_t> from_10_to_2(vector<uint32_t> v_10)
{
    vector<uint32_t> v_2;
    do
    {
        uint64_t r = 0;
        unsigned int i = 0;
        for(unsigned int j = 0; j < v_10.size(); ++j)
        {
            r = r * S_10 + v_10[j];
            if(i || r >> N)
            {
                v_10[i++] = r >> N;
                r = (uint32_t)r;
            }
        }
        v_10.resize(i);
        v_2.push_back(r);
    }
    while(v_10.size());
    return v_2;
}

vector<uint32_t> from_string(const string &s)
{
    vector<uint32_t> v_10;
    unsigned int i = 0;
    while(i < s.size())
    {
        unsigned int j_max = v_10.size() ? 9 : s.size() % 9;
        v_10.push_back(0);
        for(unsigned int j = 0; j < j_max; ++j)
        {
            v_10.back() = v_10.back() * 10 + (s[i++] - '0');
        }
    }
    return(from_10_to_2(v_10));
}

void print(vector<uint32_t> v_2)
{
    reverse(v_2.begin(), v_2.end());
    vector<uint32_t> v_10 = from_2_to_10(v_2);
    for(unsigned int i = 0; i < v_10.size(); ++i)
    {
        if(i)
        {
            for(int32_t j = S_10 / 10 - 1; j && j >= v_10[v_10.size() - 1 - i]; j /= 10)
            {
                cout << "0";
            }
        }
        cout << v_10[v_10.size() - 1 - i];
    }
    cout << endl;
}

vector<uint16_t> to_v_bool(const vector<uint32_t> &v)
{
    vector<uint16_t> b(v.size() * N);
    for(uint32_t i_v = 0; i_v < v.size(); ++i_v)
    {
        for(uint32_t i_b = (i_v + 1) * N - 1, n = v[v.size() - 1 - i_v]; n; n >>= 1)
        {
            b[i_b--] = bool(1 & n);
        }
    }
    return b;
}

vector<uint32_t> from_v_bool(const vector<uint16_t> &b)
{
    vector<uint32_t> v(b.size() / N);
    for(uint32_t i_v = v.size() - 1, i_b = 0; i_b < b.size(); --i_v)
    {
        for(uint32_t n = (uint32_t)1 << N - 1; n; n >>= 1)
        {
            if(b[i_b++])
            {
                v[i_v] |= n;
            }
        }
    }
    return v;
}

vector<uint32_t> division_2(const vector<uint32_t> &v1, const vector<uint32_t> &v2, const bool flag_q)
{
    vector<uint16_t> b1 = to_v_bool(v1);
    vector<uint16_t> b2 = to_v_bool(v2);
    vector<uint16_t> q(b1.size() - b2.size() + N);
    uint64_t sx_2 = 0;
    for(; !b2[sx_2]; ++sx_2);
    uint64_t n_b2 = b2.size() - sx_2;
    for(uint64_t sx_1 = 0, dx_1 = n_b2 - 1; dx_1 < b1.size(); ++sx_1, ++dx_1)
    {
        uint64_t i = sx_1 && b1[sx_1 - 1] ? n_b2 : 0;
        for(; i < n_b2 && b1[sx_1 + i] == b2[sx_2 + i]; ++i);
        if(i == n_b2 || !b2[sx_2 + i])
        {
            q[dx_1 + N - b2.size()] = 1;
            for(uint64_t j = 0; j < n_b2; ++j)
            {
                if(b2[b2.size() - 1 - j])
                {
                    for(uint64_t k = dx_1 - j; b1[k] = !b1[k]; --k);
                }
            }
        }
    }
    return(flag_q ? from_v_bool(q) : from_v_bool(b1));
}

int main()
{
    vector<uint32_t> a = from_string("123456789987654321");
    vector<uint32_t> b = from_string("157988");

    auto start1 = high_resolution_clock::now();
    vector<uint32_t> q = division_2(a, b, true);
    auto stop1 = high_resolution_clock::now();
    auto duration1 = duration_cast<milliseconds>(stop1 - start1);

    vector<uint32_t> r = division_2(a, b, false);

    print(q);
    cout << endl;
    print(r);
    cout << endl;
    cout << duration1.count() << " ms" << endl;
}
Detto ciò, è vero che il vettore booleano è circa 3,3 volte più lungo della stringa, ma nella versione con le stringhe ci sono comunque varie operazioni con interi, senza dimenticare poi la parte relativa ai "tentativi"... probabilmente non mi resta che arrendermi all'evidenza, ma nella mia ignoranza trovo questa differenza nelle prestazioni abbastanza controintuitiva, che ne pensate?

7 Risposte

  • Re: Divisione tra big int

    Il metodo da te utilizzato può essere il più corretto, ma se alla fine non sai esattamente come il compilatore ha tradotto il tuo programma nelle istruzioni per il processore, non puoi sapere esattamente il motivo per cui un metodo risulti più efficiente dell'altro. Hai provato a tradurre il tuo codice C++ in plain C? Ottieni gli stessi risultati? Hai provato a modificare le impostazioni di ottimizzazione del compilatore? La memoria è sempre ben allineata in entrambi i casi? Sono i primi pensieri che mi passano per la mente, ma potrei sbagliarmi.
  • Re: Divisione tra big int

    Unqualunque ha scritto:


    Il metodo da te utilizzato può essere il più corretto, ma se alla fine non sai esattamente come il compilatore ha tradotto il tuo programma nelle istruzioni per il processore, non puoi sapere esattamente il motivo per cui un metodo risulti più efficiente dell'altro.
    Grazie per la risposta. Cmq sì, mi rendo conto che senza conoscere un po' di linguaggio assembly e di teoria della complessità computazionale risulta difficile venirne a capo.

    Unqualunque ha scritto:


    Hai provato a tradurre il tuo codice C++ in plain C?
    No, ma alla fine non è che stia utilizzando chissà quale funzionalità avanzata del C++; così su due piedi mi sembra che il tutto si riduca (nella versione in base 2) all'utilizzo dei vector, la cui dimensione è stata opportunamente inizializzata (e quindi non si verifica nessuna riallocazione).

    Unqualunque ha scritto:


    Hai provato a modificare le impostazioni di ottimizzazione del compilatore?
    Un pochino sì, i risultati migliori li ottengo compilando col flag -O3, ma in ogni caso la versione con le stringhe in base 10 risulta sempre più veloce di quella con i vettori booleani in base 2.

    Unqualunque ha scritto:


    La memoria è sempre ben allineata in entrambi i casi?
    Mi dispiace, ma non conosco l'argomento.


    Magari appena ho un po' di tempo provvedo a postare un codice compilabile, così se qualcuno volesse può anche testarlo.
  • Re: Divisione tra big int

    Ho modificato il post iniziale inserendo dei codici compilabili.
    Dal momento che ho riscontrato dei problemi nell'inserire tante cifre all'interno dei tag code, vi riporto i numeri del test di cui parlo nel post iniziale:

    DIVIDENDO:
    91631459163145353416637717892856160901005398276054188472482060418235004021118210956351616940266536015638278497228179865331685588676587368476039039521391666504562499501006032461741356208569334792826415730458568082586985092566358164760971856062743144591967702966773802797086798420840927511694048799892032516936567567888761350054189508355254823311468650420318287411043675431491402945929273575156608815054256449923481134020757101485517427325300173671577239211345391501538723135242346199478651584810083452961559902422393096590469223822979141833120795204487015214756301220909487586448959743534720378345253588662768447793312163215237404409007293575791105084381107643128252542936804107364646998730996726211510521848690899641253638244657890949703716394445077926240881029392372450361666511119080385849163145353416637717892856160901005398276054188472482060418235004021118210956351616940266536015638278497228179865331685588676587368476039039521391666504562499501006032461741356208569334792826415730458568082586985092566358164760971856062743144591967702966773802797086798420840927511694048799892032516936567567888761350054189508355254823311468650420318287411043675431491402945929273575156608815054256449923481134020757101485517427325300173671577239211345391501538723135242346199478651584810083452961559902422393096590469223822979141833120795204487015214756301220909487586448959743534720378345253588662768447793312163215237404409007293575791105084381107643128252542936804107364646998730996726211510521848690899641253638244657890949703716394445077926240881029392372450361666511119080385849860362072699515631391631453534166377178928561609010053982760541884724820604182350040211182109563516169402665360156382784972281798653316855886765873684760390395213916665045624995010060324617413562085693347928264157304585680825869850925663581647609718560627431445919677029667738027970867984208409275116940487998920325169365675678887613500541895083552548233114686504203182874110436754314914029459292735751566088150542564499234811340207571014855174273253001736715772392113453915015387231352423461994786515848100834529615599024223930965904692238229791418331207952044870152147563012209094875864489597435347203783452535886627684477933121632152374044090072935757911050843811076431282525429368041073646469987309967262115105218486908996412536382446578909497037163944450779262408891631453534166377178928561609010053982760541884724820604182350040211182109563516169402665360156382784972281798653316855886765873684760390395213916665045624995010060324617413562085693347928264157304585680825869850925663581647609718560627431445919677029667738027970867984208409275116940487998920325169365675678887613500541895083552548233114686504203182874110436754314914029459292735751566088150542564499234811340207571014855174273253001736715772392113453915015387231352423461994786515848100834529615599024223930965904692238229791418331207952044870152147563012209094875864489597435347203783452535886627684477933121632152374044090072935757911050843811076431282525429368041073646469987309967262115105218486908996412536382446578909497037163944450779262408810293923724503616665111190803858498603620726995156313065963600401460803778463800984001102939237245036166651111908038584986036207269951563130659636004014608037784638009840010659636004014608037784638009840019860362072699515631306596360040146080377846380098400135341663771789285616090100539827605418847248206041823500402111821095635161694026653601563827849722817986533168558867658736847603903952139166650456249950100603246174135620856933479282641573045856808258698509256635816476097185606274314459196770296677380279708679842084092751169404879989203251693656756788876135005418950835525482331146865042031828741104367543149140294592927357515660881505425644992348113402075710148551742732530017367157723921134539150153872313524234619947865158481008345296155990242239309659046922382297914183312079520448701521475630122090948758644895974353472037834525358866276844779331216321523740440900729357579110508438110764312825254293680410736464699873099672621151052184869089964125363824465789094970371639444507792624088102939237245036166651111908038584986036207269951563130659636004014608037784638009840019163145916314535341663771789285616090100539827605418847248206041823500402111821095635161694026653601563827849722817986533168558867658736847603903952139166650456249950100603246174135620856933479282641573045856808258698509256635816476097185606274314459196770296677380279708679842084092751169404879989203251693656756788876135005418950835525482331146865042031828741104367543149140294592927357515660881505425644992348113402075710148551742732530017367157723921134539150153872313524234619947865158481008345296155990242239309659046922382297914183312079520448701521475630122090948758644895974353472037834525358866276844779331216321523740440900729357579110508438110764312825254293680410736464699873099672621151052184869089964125363824465789094970371639444507792624088102939237245036166651111908038584916314535341663771789285616090100539827605418847248206041823500402111821095635161694026653601563827849722817986533168558867658736847603903952139166650456249950100603246174135620856933479282641573045856808258698509256635816476097185606274314459196770296677380279708679842084092751169404879989203251693656756788876135005418950835525482331146865042031828741104367543149140294592927357515660881505425644992348113402075710148551742732530017367157723921134539150153872313524234619947865158481008345296155990242239309659046922382297914183312079520448701521475630122090948758644895974353472037834525358866276844779331216321523740440900729357579110508438110764312825254293680410736464699873099672621151052184869089964125363824465789094970371639444507792624088102939237245036166651111908038584986036207269951563139163145353416637717892856160901005398276054188472482060418235004021118210956351616940266536015638278497228179865331685588676587368476039039521391666504562499501006032461741356208569334792826415730458568082586985092566358164760971856062743144591967702966773802797086798420840927511694048799892032516936567567888761350054189508355254823311468650420318287411043675431491402945929273575156608815054256449923481134020757101485517427325300173671577239211345391501538723135242346199478651584810083452961559902422393096590469223822979141833120795204487015214756301220909487586448959743534720378345253588662768447793312163215237404409007293575791105084381107643128252542936804107364646998730996726211510521848690899641253638244657890949703716394445077926240889163145353416637717892856160901005398276054188472482060418235004021118210956351616940266536015638278497228179865331685588676587368476039039521391666504562499501006032461741356208569334792826415730458568082586985092566358164760971856062743144591967702966773802797086798420840927511694048799892032516936567567888761350054189508355254823311468650420318287411043675431491402945929273575156608815054256449923481134020757101485517427325300173671577239211345391501538723135242346199478651584810083452961559902422393096590469223822979141833120795204487015214756301220909487586448959743534720378345253588662768447793312163215237404409007293575791105084381107643128252542936804107364646998730996726211510521848690899641253638244657890949703716394445077926240881029392372450361666511119080385849860362072699515631306596360040146080377846380098400110293923724503616665111190803858498603620726995156313065963600401460803778463800984001065963600401460803778463800984001986036207269951563130659636004014608037784638009840013534166377178928561609010053982760541884724820604182350040211182109563516169402665360156382784972281798653316855886765873684760390395213916665045624995010060324617413562085693347928264157304585680825869850925663581647609718560627431445919677029667738027970867984208409275116940487998920325169365675678887613500541895083552548233114686504203182874110436754314914029459292735751566088150542564499234811340207571014855174273253001736715772392113453915015387231352423461994786515848100834529615599024223930965904692238229791418331207952044870152147563012209094875864489597435347203783452535886627684477933121632152374044090072935757911050843811076431282525429368041073646469987309967262115105218486908996412536382446578909497037163944450779262408810293923724503616665111190803858498603620726995156313065963600401460803778463800984001
    DIVISORE:
    83579263145353416637717892856166314535341669263145353416637717892856166314535341663771789263145353416637717892856166314535341663771786314535341663771789631453534166377178937717863145353416637717892856160901005398276054188631453534166377178928561609010053982760541884724820647248206631631453534166377178928561609010053982760541884724820645353416637717892856314535341663771789285616090100539827605418847248206616090100539827605418847248206928561609010053982760541884724820609010053982760541884724820646631453534166377178928561609010053982760541884724820680078357926314535341663771789285616631453534166926314535341663771789285616631453534166377178926314535341663771789285616631453534166377178631453534166377178963145353416637717893771786314535341663771789285616090100539827605418863145353416637717892856160901005398276054188472482064724820663163145353416637717892856160901005398276054188472482064535341663771789285631453534166377178928561609010053982760541884724820661609010053982760541884724820692856160901005398276054188472482060901005398276054188472482064663145353416637717892856160901005398276054188472482068007
    Quella con le stringhe, come già detto, è una vecchia libreria scritta tempo fa, quindi lasciate perdere se ci sono ottimizzazioni da fare, invece se avete qualche consiglio sulla versione coi vettori sono tutto orecchi!
  • Re: Divisione tra big int

    My 2cents
    Così di primo acchito mi sembra che la versione "binaria" sia O(n^4) mentre la versione "decimale" sia O(n^3), il che rende inutili le ottimizzazioni sperate e spiega la differenza prestazionale.
    In altre parole: nella funzione division_2 ci sono 4 cicli annidati, nella funzione division sono 3
  • Re: Divisione tra big int

    shodan ha scritto:


    My 2cents
    Così di primo acchito mi sembra che la versione "binaria" sia O(n^4) mentre la versione "decimale" sia O(n^3), il che rende inutili le ottimizzazioni sperate e spiega la differenza prestazionale.
    In altre parole: nella funzione division_2 ci sono 4 cicli annidati, nella funzione division sono 3
    Come già detto so poco o nulla di teoria della complessità computazionale, ma presumo che molto dipenda anche dalle operazioni che poi i cicli effettivamente eseguono. In ogni caso è vero che nella funzione division() ci sono solo 3 cicli annidati, ma essa richiama le funzioni subtraction(), multiplication() e compare_abs(), che a loro volta contengono altri cicli.


    Per caso qualcuno ha testato il codice? Ottenete risultati simili ai miei?
  • Re: Divisione tra big int

    La teoria della complessita' computazionale non va nel dettaglio delle istruzioni macchina, ma SOLO sul NUMERO di volte che tale operazioni vengono eseguite e tale numero deve DIPENDERE da una qualche misura sulla DIMENSIONE dei dati di ingresso.

    Nel caso dell'aritmetica in precisione arbitraria, tale DIMENSIONE e' il numero di CIFRE che servono per rappresentare i numeri da trattare. Che la base sia 2, 10, 256 65536, cambia poco, anzi nulla.
    Quello che interessa e': se RADDOPPIO il numer di cifre, il TEMPO per fare l'operazione rimane UGUALE (non cambia), aumenta in modo LINEARE (cioe' radoppia anche lui) o ESPONENZIALE? E se aumenta in modo esponenziale, con QUALE esponente? 2,3,4 o di piu'?

    Questa informazione e' utile sopprattutto in ambito teorico, ma qualche volta anche in ambito ""pratico"".
    Puo' capitare che si faccia un'implementazione di un software che sul proprio computer funziona correttamente, ma quando va in produzione sembra non funzionare e l'unica differenza sembra essere che durante i test si sono usati un numero limitato di dati, mentre in produzione i dati sono molti di piu'. Se l'algoritmo che si e' implementato ha una complessita' computazionale 2^n, ad esempio, i tempi di calcolo aumentano in modo rapidissimo: se con 10 elementi impiega 1 secondo, con 20 elementi impiega 1000 secondi ma con 30 elementi impiega 1000.000 di secondi!

    Questo per dire che se la stessa operazione con un'implementazione sta' 100 millisecondi, e con un'altra 300 millisecondi, e' si vero che la seconda implementazione e' meno efficiente della prima, ma tutto sommato NON E' IMPORTANTE.
    L'importante e' che aumentando la dimensione dei dati, l'aumento dei tempi di calcolo NON SIA esponenziale, MA LINEARE o LOGARITMICO (o meno).

    Per fare un esempio: ricerca in una lista di N elementi.
    Se gli elemento NON SONO ordinati, devi necessariamente scandire la lista dal primo all'ultimo elemento alla ricerca dell'elemento desiderato. Questo algoritmo richiede, MEDIAMENTE, N/2 confronti.
    Se gli elemento SONO ordinati, allora puoi fare una ricerca binaria con un numero di test pari a LOG2(N).
    Quindi, se hai una lista da 100.000 di elementi, nel primo caso ti servono mediamente 500.000 confronti, nel secondo 20 confronti.

    20 confronti rispetto a 500.000 sono un ""ragionevole"" miglioramento


    Comunque, ci sono 2 aspetti da considerare nell'implementazione delle operazioni aritmetiche in precisione arbitraria:

    1) la BASE usata per rappresentare i numeri. Piu' la base e' grande, piu' sara' efficiente l'implementazione. Se in base 2 devi manipolare diciamo 100.000 di bit, in base 10 dovrai manipolare SOLO 434000 cifre decimali, 125000 byte/uint8, 62500 uint16, 31250 uint32, oppure 15625 uint64. Supponendo di usare uint32, dovrai manipolare 1/3 delle delle cifre binarie. Gia' questo dovrebbe rendere l'algoritmo 3 volte piu' veloce.

    Ma questo NON BASTA

    2) l'algoritmo usato: ci sono algoritmi INEFFICIENTI (vedasi ad esempio il Bubble Sort) ed altri molto piu' efficienti (vedasi il Quick Sort). Gli algoritmi efficienti sono tali perche sfruttano delle proprieta' dei dati da trattare. In genere queste proprieta' sono assai ""subdole"", nel senso che sono proprieta' molto speciali che non sono evidenti ad un'osservatore NON ESPERTO.

    Questo per dire che non sarebbe male, invece di provare a fare implementazioni ""caserecce"" (le ho fatte anch'io, che ti credi ), almeno leggere un po' di teoria sull'argomento. Questo ti permette di capire perche' certi algortimi sono piu' efficienti degli altri. Poi' anche tra gli algoritmi efficineti ci sono quelli facili da implementare e quelli piu' complicati. Per fare pratica, in genere basta implementare quello piu' semplice.

    Fonti per l'aritmetica in precisione arbitraria sono:

    Handbook of Applied Cryptography: libro strepitoso che non e' piu' in commercio ma di cui si trova il PDF (legale)



    Ovviamente "The Art of Computer Programming" di Knuth, volume 2 "Seminumerical Algorithms".
    Puoi rovare il PDF abbastanza facilmente

    BigNum Math: descrizione dell'implementazione della libreria GNU MP (Multi Precision library)
    Puoi rovare il PDF abbastanza facilmente

    Ultima nota: ti conviene ragionare in termini di "uint8[]" ... unit32[]" piu' che "std::vector", per il semplice fatto che la struttura dati ""vector"" e' stata pensata per poter essere RIDIMENSIONATA, mentre e' possibile prevedere A PRIORI la dimensione dei vettori necessari per contenere un numero intero o il risultato dell'operazione, con un piccolissimo spreco.
    Ad esempio:
    nella somma di due numeri di N1 e N2 cifre, AL PIU' puoi avere MAX(N1,N2)+1 cifre (999+1 -> 1000)
    nel prodotto di due numeri di N1 e N2 cifre, AL PIU' puoi avere N1+N2 cifre (999*99 -> 98901)
    nella divisione, con N1 >= N2, AL PIU' il risultato potra' avere N1-N2+1 cifre.

    Inoltre, in questo modo hai TOTALE CONTROLLO sulle operazioni assembler usate. Ma non solo, puoi sempre sostituire la funzione C che implementa un'operazione con il suo equivalente in Assember.
  • Re: Divisione tra big int

    @migliorabile grazie per l'infarinatura generale sulla complessità computazionale e per il materiale che mi hai indicato.

    migliorabile ha scritto:


    ...
    Questo per dire che se la stessa operazione con un'implementazione sta' 100 millisecondi, e con un'altra 300 millisecondi, e' si vero che la seconda implementazione e' meno efficiente della prima, ma tutto sommato NON E' IMPORTANTE.
    L'importante e' che aumentando la dimensione dei dati, l'aumento dei tempi di calcolo NON SIA esponenziale, MA LINEARE o LOGARITMICO (o meno).
    Nel mio caso, avendo a che fare con due vettori, non so quale sia il modo giusto di definire "la dimensione dei dati"; in ogni caso per un rapporto r=8 tra il numero di cifre del dividendo e quelle del divisore, facendo dei test approssimativi riscontro ad ogni raddoppio della dimensione dei dati un aumento dei tempi pari a 3,5 volte nel caso della versione string e a 4 volte nel caso della versione vector.
    Ovviamente, come già detto nel post iniziale, i vettori booleani della nuova versione sono circa 3,3 volte più lunghi delle stringhe utilizzate nella vecchia versione.


    migliorabile ha scritto:


    1) la BASE usata per rappresentare i numeri. Piu' la base e' grande, piu' sara' efficiente l'implementazione.
    Questo è il motivo (ma non il solo) per cui nella nuova libreria che sto scrivendo opero in base 2^32, ed effettivamente in relazione a +, - e * ho riscontrato un grande miglioramento, ma sospetto che con la divisione, almeno per l'algoritmo che sto utilizzando, la faccenda si complichi un po'. Come scritto nel post iniziale:

    Nippolo ha scritto:


    - l'algoritmo di divisione che sto utilizzando, tralasciando le varie versioni implementate e le varie ottimizzazioni (si spera) adottate, è quello della classica divisione in colonna che viene insegnata alle elementari;

    - le singole cifre q_i del quoziente Q, a seconda della base b adottata, varieranno nell'intervallo [0;b-1]. Una volta assicuratoci con un semplice confronto che q_i!=0, possiamo facilmente fissare q_i_MAX, ossia il massimo valore che quella determinata cifra del quoziente può assumere. Fissato q_i_MAX, per giungere al q_i effettivo bisognerà ovviamente fare dei "tentativi", e il punto è proprio questo, ossia: maggiore è la base b adottata, maggiori saranno in media i tentativi da fare per giungere al valore di q_i corretto;

    - partendo dalla suddetta osservazione, ho pensato che la cosa migliore sarebbe stata quella di eliminare i "tentativi" effettuando la divisione in base 2, dove infatti q_i, una volta escluso lo 0 con un semplice confronto, non può che essere 1.
    Sei d'accordo con queste considerazioni?

    migliorabile ha scritto:


    Questo per dire che non sarebbe male, invece di provare a fare implementazioni ""caserecce"" (le ho fatte anch'io, che ti credi ), almeno leggere un po' di teoria sull'argomento.
    Non mi piacciono gli spoiler, altrimenti poi che sfizio ci sta!

    migliorabile ha scritto:


    Ultima nota: ti conviene ragionare in termini di "uint8[]" ... unit32[]" piu' che "std::vector", per il semplice fatto che la struttura dati ""vector"" e' stata pensata per poter essere RIDIMENSIONATA, mentre e' possibile prevedere A PRIORI la dimensione dei vettori necessari per contenere un numero intero o il risultato dell'operazione, con un piccolissimo spreco.
    Ad esempio:
    nella somma di due numeri di N1 e N2 cifre, AL PIU' puoi avere MAX(N1,N2)+1 cifre (999+1 -> 1000)
    nel prodotto di due numeri di N1 e N2 cifre, AL PIU' puoi avere N1+N2 cifre (999*99 -> 98901)
    nella divisione, con N1 >= N2, AL PIU' il risultato potra' avere N1-N2+1 cifre.
    In realtà già lo faccio nella nuova libreria, per esempio nel caso di division_2() che ho postato c'è la riga di codice:
    vector<uint16_t> q(b1.size() - b2.size() + N);
    Alla fine utilizzo i vector per comodità, ma definendone la dimensione al momento della dichiarazione, posso tranquillamente sostituirli con array allocati dinamicamente. Appena ho tempo provo a fare questa sostituzione e vi faccio sapere se riscontro differenze significative.
    *PROVATO, i tempi restano gli stessi.
Devi accedere o registrarti per scrivere nel forum
7 risposte