Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 01/12/2021, 14:22

vict85 ha scritto:Ho notato che questo codice fa qualcosa che probabilmente non è quello che immagini:
Codice:
const vector<uint32_t> &w1 = v1.size() < v2.size() ? v1 : v2;
const vector<uint32_t> &w2 = w1 == v1 ? v2 : v1;

infatti il confronto w1 == v1 fa il confronto tra vector e non di puntatori. Per confrontare i puntatori devi scrivere &w1 == &v1.

Per capire, guarda
https://godbolt.org/z/8zfx5r1MP
e noterai che che viene chiamata l'uguaglianza di vector, mentre con &w1 == &v1 si confrontano due registri.

Dal punto di vista del risultato non dovrebbero esserci differenze, ma confrontare i vector risulta un inutile rallentamento, infatti il mio intento era quello di confrontare gli indirizzi. Grazie per avermelo fatto notare.

vict85 ha scritto:Per quanto riguarda la sottrazione non vedo alcun sistema per gestire i numeri negativi. Insomma, se \(v_1 < v_2\) ti viene un risultato poco sensato.

Queste funzioni le sto utilizzando per implementare una classe big_int. Tale classe possiede due membri, un vector<uint32_t> e un char per tenere traccia del segno. Nell'overload di operator -(const big_int&, const big_int&) richiamo la funzione subtraction() in modo che il primo parametro sia in valore assoluto maggiore al secondo. Mi scuso, avrei dovuto specificarlo.

vict85 ha scritto:Per l'addizione, sei sicuro che le due funzioni diano lo stesso risultato? A occhio mi sembra che la seconda non stia tenendo conto del riporto finale.

Sì, dovrebbero essere equivalenti. Il riporto finale dovrebbe essere assicurato dal || r presente nella condizione del for.
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 27 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 02/12/2021, 02:39

Vabbè, tralasciando le trascurabili questioni di ottimizzazione che ho sollevato negli ultimi post, ossia dando per concluse le funzioni di addizione, sottrazione e moltiplicazione, non mi resta che implementare la divisione intera.
Questa è la funzione (si tratta più o meno del classico algoritmo insegnato a scuola) che avevo implementato qualche tempo fa nella versione con le stringhe:

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

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


Mi rendo conto che non è il massimo e che presenta ampi margini di miglioramento, ma il punto è che questo stesso approccio, se utilizzato nella versione con i vector, in cui al contrario del codice appena postato considero gruppi di più cifre alla volta, temo possa rivelarsi molto meno performante. Il motivo sta nell'individuazione di "q", ossia delle singole cifre (o gruppi di cifre, a seconda della versione) del quoziente; infatti considerando una cifra alla volta, l'intervallo di appartenenza di "q" si riduce a [0;9], mentre considerando più cifre alla volta il suddetto intervallo cresce considerevolmente e quindi la ricerca di "q" richiederà in media molti più tentativi.

Nella speranza di essere stato abbastanza chiaro, volevo chiedervi se siete d'accordo con questa mia preoccupazione.
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 28 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 03/12/2021, 13:54

Provo a riformulare la questione sollevata nel precedente post: dal momento che il classico algoritmo di divisione che viene insegnato a scuola sembra essere molto più veloce considerando una cifra alla volta piuttosto che considerando gruppi di più cifre alla volta così come sto facendo nell'implementazione di questa libreria per i "big int", cosa mi conviene fare?
Prevedo due suddivisioni, una a gruppi di più cifre per +, -, *, e una a gruppi di una sola cifra per /, %?
Oppure mi conviene ricorrere ad un diverso algoritmo di divisione? E nel caso, quale?
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 29 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda apatriarca » 03/12/2021, 15:09

Come ti ho già detto in altre circostanze, devi smettere di pensare di lavorare con più cifre ma piuttosto di lavorare con cifre in una base diversa da dieci. Se una 9 cifre allora stai lavorando in una base uguale a 1000000000. A quel punto tutte le operazione del tuo codice che usano la base 10 diventano operazioni con la nuova base.
apatriarca
Moderatore
Moderatore
 
Messaggio: 5639 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 03/12/2021, 16:24

apatriarca ha scritto:Come ti ho già detto in altre circostanze, devi smettere di pensare di lavorare con più cifre ma piuttosto di lavorare con cifre in una base diversa da dieci. Se una 9 cifre allora stai lavorando in una base uguale a 1000000000. A quel punto tutte le operazione del tuo codice che usano la base 10 diventano operazioni con la nuova base.

Ok, allora riformulo ancora una volta: il classico algoritmo di divisione insegnato a scuola è, a differenza di quanto accade per addizione, sottrazione e moltiplicazione, molto più veloce in base 10 che in base 10^9.
Cosa mi conviene fare? Utilizzare una base diversa a seconda dell'operazione oppure utilizzare un diverso algoritmo di divisione?
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 30 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 05/12/2021, 15:11

Dal momento che le singole cifre del quoziente possono variare nell'intervallo $[0;b-1]$ (dove $b$ è la base numerica adottata) e che il valore $0$ può essere escluso con un semplice confronto, avrei pensato di effettuare la divisione direttamente in base $2$. A tal proposito facendo qualche ricerca in rete penso che un vector<bitset<32>> o un vector<bool> potrebbero fare al mio caso.
Che ne pensate, vale la pena lavorarci? Ne verrebbe fuori qualcosa di abbastanza performante? E nel caso con cosa mi converrebbe lavorare? vector<bitset<32>>, vector<bool>, string o altro?


P.S.
Quanto è efficiente e come funziona internamente la funzione bitset::to_ulong()?
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 31 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 06/12/2021, 15:53

Intanto ho provato ad implementare qualcosa con i bitset restando nell'ambito dei 64 bit:

Codice:
#include <iostream>
#include <bitset>
#include <cstdint>

using namespace std;

const uint8_t N = 64;

bool operator <(const bitset<N> &b1, const bitset<N> &b2)
{
    for(int8_t i = N - 1; i >= 0; i--)
    {
        if(b1[i] != b2[i])
        {
            return b2[i];
        }
    }
    return false;
}

uint8_t msb(const bitset<N> &b)//most significant bit
{
    uint8_t i;
    for(i = N - 1; i && !b[i]; --i);
    return i;
}

uint64_t division(bitset<N> b1, bitset<N> b2, const bool flag_q = true)
{
    bitset<N> q;
    uint8_t divisor_msb = msb(b2);
    int8_t i = (signed)msb(b1) - divisor_msb;
    if(i > 0)
    {
        b2 <<= i;
    }
    for(; i >= 0; --i, b2 >>= 1)
    {
        if(!(b1 < b2))
        {
            q[i] = true;
            for(int8_t j = i; j <= divisor_msb + i; ++j)
            {
                if(b2[j])
                {
                    for(int8_t k = j; b1[k] = !b1[k]; ++k);
                }
            }
        }
    }
    return(flag_q ? q.to_ullong() : b1.to_ullong());
}

int main()
{
    uint64_t a = 3027239782390037;
    uint64_t b = 5146;
    cout << division(bitset<N>(a), bitset<N>(b)) << endl;
    cout << a / b << endl;
    cout << division(bitset<N>(a), bitset<N>(b), false) << endl;
    cout << a % b << endl;
}


Sembra funzionare, ma la "traduzione" dell'algoritmo da una divisione tra uint64_t a una divisione tra "big_int" non mi sembra semplicissimo, soprattutto volendo mantenere prestazioni elevate.
Qualsiasi consiglio è ben accetto!
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 32 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda vict85 » 07/12/2021, 10:21

Mi sembra sospetto che tu abbia bisogno di usare questo tipo di approccio per fare una divisione di interi. bitset ha altri scopi. Non ho però avuto tempo di guardare il tuo codice. Immagino che tu stia usando un approccio sbagliato per la base \(10^9\).
vict85
Moderatore
Moderatore
 
Messaggio: 10550 di 19253
Iscritto il: 16/01/2008, 00:13
Località: Berlin

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda utente__medio » 08/12/2021, 02:15

vict85 ha scritto:Mi sembra sospetto che tu abbia bisogno di usare questo tipo di approccio per fare una divisione di interi. bitset ha altri scopi. Non ho però avuto tempo di guardare il tuo codice. Immagino che tu stia usando un approccio sbagliato per la base \( 10^9 \).

Cerco di fare il punto sulla questione della divisione tra "big_int" che ho introdotto negli ultimi post:

- 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 a scuola (la cosiddetta "divisione lunga");

- 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. Siete d'accordo con questa cosa, oppure sto dicendo una sciocchezza?

- 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$. Nel precedente post ho improvvisato una versione con i bitset per interi a 64 bit; nel frattempo credo di essere riuscito ad implementare la divisione binaria anche tra "big_int" ricorrendo a vector<bitset<32>> (visto che le funzioni di +,-,* già implementate sfruttano vector<uint32_t>), ecco il codice (ho omesso le funzioni non necessarie):

Testo nascosto, fai click qui per vederlo
Codice:
#include <iostream>
#include <algorithm>
#include <bitset>
#include <cstdint>
#include <limits>
#include <string>
#include <vector>

using namespace std;

const uint8_t N = 32;
const uint32_t S_10 = 1000000000;
const uint64_t S_2 = (uint64_t)numeric_limits<uint32_t>::max() + 1;

#define FUN(b, i) b[(i) / N][N - 1 - (i) % N]

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));
}

vector<bitset<N>> to_bitset(const vector<uint32_t> &v)
{
    vector<bitset<N>> b;
    b.reserve(v.size());
    for(unsigned int i = 0; i < v.size(); ++i)
    {
        b.push_back(bitset<N>(v[v.size() - 1 - i]));
    }
    return b;
}

vector<uint32_t> from_bitset(const vector<bitset<N>> &b)
{
    vector<uint32_t> v;
    v.reserve(b.size());
    for(unsigned int i = 0; i < b.size(); ++i)
    {
        v.push_back(b[b.size() - 1 - i].to_ulong());
    }
    return v;
}

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<uint32_t> division_2(vector<bitset<N>> b1, const vector<bitset<N>> &b2, const bool flag_q)
{
    vector<bitset<N>> q(b1.size() - b2.size() + 1);
    uint64_t b1N = b1.size() * N;
    uint64_t b2N = b2.size() * N;
    uint64_t b2_i = 0;
    for(; !FUN(b2, b2_i); ++b2_i);
    uint64_t b2_n = b2N - b2_i;
    for(uint64_t i = b2_n - 1, b1_i = i + 1 - b2_n; i < b1N; ++i, ++b1_i)
    {
        uint64_t j = b1_i && FUN(b1, b1_i - 1) ? b2_n : 0;
        for(; j < b2_n && FUN(b1, b1_i + j) == FUN(b2, b2_i + j); ++j);
        if(j == b2_n || !FUN(b2, b2_i + j))
        {
            FUN(q, i + N - b2N) = true;
            for(uint64_t k_1 = 0; k_1 < b2_n; ++k_1)
            {
                if(FUN(b2, b2N - 1 - k_1))
                {
                    for(uint64_t k_2 = i - k_1; FUN(b1, k_2) = !FUN(b1, k_2); --k_2);
                }
            }
        }
    }
    return(flag_q ? from_bitset(q) : from_bitset(b1));
}

int main()
{
    vector<uint32_t> a = from_string("1234567890556677889925450987654321");
    vector<uint32_t> b = from_string("835792468007");
    vector<uint32_t> q = division_2(to_bitset(a), to_bitset(b), true);
    vector<uint32_t> r = division_2(to_bitset(a), to_bitset(b), false);
    print(q);
    cout << endl;
    print(r);
    cout << endl;
}

Il codice sembra funzionare, ma considerazioni e consigli al riguardo sarebbero davvero ben accetti;

- passiamo ora alle note dolenti... ero abbastanza soddisfatto della divisione appena postata, tuttavia confrontandola con la versione postata qui (divisione in base $10$ che fa parte di una vecchia libreria sui "big_int" basata sulle stringhe, che ho implementato tempo fa), ne esce sconfitta, e anche di parecchio! Per esempio nella divisione tra un numero a 4180 cifre ed uno a 556 cifre, la vecchia versione con le stringhe impiega 50ms, mentre questa nuova 110ms (misurando solo l'esecuzione delle funzioni di divisione).
Eppure la funzione appena postata mi sembra abbastanza "compatta", priva di "tentativi" e basata su operazioni "veloci"... secondo voi dove possono celarsi le criticità che la rendono più lenta della vecchia versione basata sulle stringhe?
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 33 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Differenza di prestazioni con implementazioni simili

Messaggioda vict85 » 10/12/2021, 15:35

Per trovare i coefficienti si può usare penso una ricerca binaria. Siccome moltiplicazione e confronto hanno peso \((O(n_b)\) e la ricerca ha un costo \(O(\log_2(b))\) allora il costo è \(O(\log_2(b)n_b)\).D'altra parte \(n_{10^9} = 9^{-1} n_{10}\) e \(\log_2(10^9) = 9\log_2(10)\), quindi mi sembra che il calcolo del coefficiente ha complessità costante al cambio della base. Però il numero di coefficienti dovrebbe essere 9 volte più piccolo, e così anche la sottrazione. Quindi mi aspetterei performance migliori al crescere della base.
vict85
Moderatore
Moderatore
 
Messaggio: 10553 di 19253
Iscritto il: 16/01/2008, 00:13
Località: Berlin

PrecedenteProssimo

Torna a Informatica

Chi c’è in linea

Visitano il forum: Nessuno e 1 ospite