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

Messaggioda vict85 » 23/11/2021, 14:46

Con il mio pc, compilando con visual studio e con mille cose aperte, la versione string è più rapida in debug mentre più lenta in release (non vi è invece differenza tra versioni del C++). Nota che ho ridotto un po' il tuo codice mettendo dei reserve e usando i template per eliminare la duplicazione. Però l'algoritmo per la moltiplicazione non va bene. Vedo se ho tempo più tardi di scrivere un algoritmo più efficiente.

La mia riscrittura usando i template:
Codice:
#include <iostream>
#include <cstdint>
#include <chrono>
#include <string>
#include <vector>

using namespace std;
using namespace std::chrono;

template < typename NumberRepType >
NumberRepType
addition( const NumberRepType& n1, const NumberRepType& n2 )
{
    const uint32_t S1 = size( n1 );
    const uint32_t S2 = size( n2 );
    const uint32_t SM = max( S1, S2 );

    NumberRepType n3;
    n3.reserve( SM + 1 );  // reserve representation size

    uint32_t n = 0;
    for ( uint32_t i = 0; i < SM; ++i )
    {
        if ( i < S1 )
        {
            n += ( n1[ i ] - '0' );
        }
        if ( i < S2 )
        {
            n += ( n2[ i ] - '0' );
        }
        n3.push_back( static_cast< uint8_t >( ( n % 10 ) + '0' ) );
        n /= 10;
    }
    if ( n > 0 )
    {
        n3.push_back( '1' );
    }
    return n3;
}

template < typename NumberRepType >
NumberRepType
multiplication( const NumberRepType& n1, const NumberRepType& n2 )
{
    const uint32_t S1 = size( n1 );
    const uint32_t S2 = size( n2 );

    NumberRepType n3;
    n3.reserve( ( S1 * S2 ) + 1 );
    NumberRepType temp;
    for ( uint32_t i = 0; i < S1; ++i )
    {
        temp.clear( );
        uint32_t n = 0;
        for ( uint32_t j = 0; j < i; ++j )
        {
            temp.push_back( '0' );
        }
        for ( uint32_t j = 0; j < S2; ++j )
        {
            n += ( n1[ i ] - '0' ) * ( n2[ j ] - '0' );
            temp.push_back( n % 10 + '0' );
        }
        n /= 10;
        if ( n > 0 )
        {
            temp.push_back( n + '0' );
        }
        n3 = addition< NumberRepType >( n3, temp );
    }
    return n3;
}

int
main( )
{
    cout << "calcolo di 12345^1000 (versione string): ";
    string a = "54321";
    string b = "1";
    auto start1 = high_resolution_clock::now( );
    for ( unsigned int i = 0; i < 1000; ++i )
    {
        b = multiplication( a, b );
    }
    auto stop1 = high_resolution_clock::now( );
    auto duration1 = duration_cast< milliseconds >( stop1 - start1 );
    cout << duration1.count( ) << " ms" << endl;
    //-----------------------------------------------------------
    cout << "calcolo di 12345^1000 (versione vector): ";
    vector< uint8_t > c = {5, 4, 3, 2, 1};
    vector< uint8_t > d = {1};
    auto start2 = high_resolution_clock::now( );
    for ( unsigned int i = 0; i < 1000; ++i )
    {
        d = multiplication( c, d );
    }
    auto stop2 = high_resolution_clock::now( );
    auto duration2 = duration_cast< milliseconds >( stop2 - start2 );
    cout << duration2.count( ) << " ms" << endl;
}
vict85
Moderatore
Moderatore
 
Messaggio: 10539 di 19253
Iscritto il: 16/01/2008, 00:13
Località: Berlin

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

Messaggioda utente__medio » 23/11/2021, 17:06

vict85 ha scritto:Con il mio pc, compilando con visual studio e con mille cose aperte, la versione string è più rapida in debug mentre più lenta in release (non vi è invece differenza tra versioni del C++).

Vabbè mi sembra di capire che al variare di hardware, compilatore e settaggi, ognuno ottenga un risultato diverso! :D

vict85 ha scritto:Nota che ho ridotto un po' il tuo codice mettendo dei reserve e usando i template per eliminare la duplicazione.

I reserve() nella versione ottimizzata li utilizzo anche io, qui li ho omessi per semplificare.
In ogni caso per quanto riguarda la moltiplicazione non è sufficiente
Codice:
n3.reserve(n1.size() + n2.size())

?

vict85 ha scritto:Però l'algoritmo per la moltiplicazione non va bene. Vedo se ho tempo più tardi di scrivere un algoritmo più efficiente.

Quello postato è il classico algoritmo che si utilizza anche su carta, e le uniche ottimizzazione che mi vengono in mente sono quella di determinare l'ordine dei due fattori al fine di minimizzare il numero di chiamate ad addition() e quella di rimuovere l'aggiunta di zeri finali andando a sommare direttamente a partire dalla cifra in esame.
Inoltre
utente__medio ha scritto:per la moltiplicazione potrei adottare algoritmi come quello di Karatsuba, ma a quel punto presumo che dovrei abbandonare la suddivisione fissa e adottare una suddivisione in gruppi di lunghezza variabile.

Ma se hai in mente qualcosa di diverso sarei curioso di sapere di cosa si tratta.
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 21 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

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

Messaggioda apatriarca » 23/11/2021, 18:39

Tu stai memorizzando valori da \(0\) a \(10^{10}-1\) nel tuo intero a \(64\) bit, ma nulla ti impedisce di memorizzare valori da \(0\) a \(2^{64} - 1\). L'unica difficoltà consiste nel riconoscere l'overflow per l'addizione ed estrarre i 64 bit più significativi per la moltiplicazione (ma questo è anche vero nel tuo caso) in C++ in modo portabile. Ovviamente in assembly è immediato, ma a volte il C++ si complica la vita da questo punto di vita.
apatriarca
Moderatore
Moderatore
 
Messaggio: 5630 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid

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

Messaggioda utente__medio » 24/11/2021, 02:24

@apatriarca ok, ora ho capito cosa intendevi nel precedente post. All'inizio avevo anche pensato di sfruttare "l'overflow circolare" degli interi senza segno, ma poi avendo l'impressione di andare a complicare troppo le cose, col rischio peraltro di implementare un codice meno performante, non mi sono più soffermato sulla questione, limitandomi invece a considerare numeri di 9 cifre che in ogni caso, anche con la moltiplicazione, non generano alcun overflow.
In ogni caso domani se ho tempo provo a riflettere concretamente su questo approccio.
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 22 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

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

Messaggioda vict85 » 24/11/2021, 15:38

Stai facendo molte più somme del necessario. Puoi raggruppare gli addendi in modo da calcolare un cifra del risultato per volta.

Per esempio, se i numeri sono \(\displaystyle a = a_0 + 10a_1 + 100a_2\) e \(\displaystyle b_0 + 10b_1\) allora \(ab = c_0 + 10c_1 + 10^2c_2 + 10^3c_3 + 10^4c_4\) dove:
\(\tilde{c}_0 = a_0b_0\)
\(c_0 = \tilde{c}_0 \pmod{10}\)
\(r_1 = \lfloor \tilde{c}_0 / 10\rfloor\)
\(\tilde{c}_1 = a_1b_0 + a_0b_1 + r_1\)
\(c_1 = \tilde{c}_1 \pmod{10}\)
\(r_2 = \lfloor \tilde{c}_1 / 10\rfloor\)
\(\tilde{c}_2 = a_2b_0 + a_1b_1 + r_2\)
\(c_2 = \tilde{c}_2 \pmod{10}\)
\(r_3 = \lfloor \tilde{c}_2 / 10\rfloor\)
\(\tilde{c}_3 = a_2b_1 + r_3\)
\(c_3 = \tilde{c}_3 \pmod{10}\)
\(c_4 = r_4 = \lfloor \tilde{c}_3 / 10\rfloor\)

Riguardo alla dimensione, hai ragione: \(size(n_1) + size(n_2)\) è effettivamente abbastanza.
vict85
Moderatore
Moderatore
 
Messaggio: 10540 di 19253
Iscritto il: 16/01/2008, 00:13
Località: Berlin

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

Messaggioda utente__medio » 24/11/2021, 16:39

vict85 ha scritto:Stai facendo molte più somme del necessario. Puoi raggruppare gli addendi in modo da calcolare un cifra del risultato per volta.

Per esempio, se i numeri sono...

Ma in fin dei conti non è equivalente a
utente__medio ha scritto:determinare l'ordine dei due fattori al fine di minimizzare il numero di chiamate ad addition() e a rimuovere l'aggiunta di zeri finali andando a sommare direttamente a partire dalla cifra in esame.

?
Inoltre andando a calcolare un cifra del risultato per volta senza passare per la funzione addition() non si corre il rischio, a causa delle potenziali molteplici addizioni da effettuare, di incorrere in un overflow di cui non è possibile tenere traccia?


P.S.
Sto iniziando a riflettere sull'approccio dell'overflow "circolare" suggerito da apatriarca, ma ho come l'impressione che questa strada conduca ad un codice meno performante (più lento). Sbaglio?
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 23 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

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

Messaggioda apatriarca » 24/11/2021, 20:18

Perché dovrebbe essere più lento?

Nel caso della somma tu hai per ogni cifra qualcosa come:
Codice:
uint64_t sum = a[i] + b[i] + carry;
c[i] = sum % 1000000000;
carry = sum / 1000000000;

Nel caso di 32 bit come cifre hai invece
Codice:
uint64_t sum = (uint64_t)a[i] + (uint64_t)b[i] + (uint64_t)carry;
c[i] = (uint32_t)sum; // extract low 32 bits
carry = (uint32_t)(sum >> 32); // extract high 32 bits

Puoi vedere due versioni di somme al link: https://godbolt.org/z/v7rbEdo6W. Ho rimosso tutta la parte della gestione della memoria e sono semplicemente degli array della dimensione che si suppone già essere abbastanza grande per contenere i valori che ti servono. Il calcolo nel ciclo principale nel tuo caso è il seguente in assembly:
Codice:
        mov     rcx, QWORD PTR [r11+rsi*8]
        add     rcx, QWORD PTR [r9+rsi*8]
        add     rcx, rdx
        mov     rdx, rcx
        shr     rdx, 9
        mov     rax, rdx
        mul     rbx
        shr     rdx, 11
        imul    rax, rdx, 1000000000
        sub     rcx, rax
        mov     QWORD PTR [r8+rsi*8], rcx

Nell'altro caso è il seguente
Codice:
        mov     r11d, DWORD PTR [r10+rax*4]
        mov     ecx, DWORD PTR [r9+rax*4]
        add     rcx, r11
        lea     r11, [rcx+rdx]
        add     edx, ecx
        mov     edx, edx
        mov     QWORD PTR [r8+rax*8], rdx

Anche senza sapere molto di assembly è chiaro che il secondo codice ha meno operazioni ed è quindi probabilmente più veloce.

Stai inoltre sprecando un sacco di byte memorizzando solo 9 cifre decimali in un intero a 64 bit.. Tanto vale memorizzarlo in 32 bit e fare le operazioni su 64 come ho fatto nel mio caso in basso.
apatriarca
Moderatore
Moderatore
 
Messaggio: 5631 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid

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

Messaggioda vict85 » 25/11/2021, 00:14

Sbagli, e anche di molto. Il tuo codice è ben poco performante ed è dominato dalle allocazioni di memoria. Per capire quanto sia pessimo, prova a compilare e lanciare questa versione:

Codice:
#include <chrono>
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>

using namespace std;
using namespace std::chrono;

template<typename NumberRepType>
void
multiplication(NumberRepType& result, NumberRepType const& a, NumberRepType const& b)
{
    uint32_t const aSize = size(a);
    uint32_t const bSize = size(b);
    uint32_t const resultSize = aSize + bSize;

    result.clear();
    uint32_t r = 0;
    for (uint32_t S = 0; S < resultSize - 1; ++S)
    {
        int32_t i = min(aSize - 1, S);
        int32_t j = S - i;
        uint32_t v = r;
        for (; i > -1 && j < bSize; i--, j++)
        {
            uint32_t const ai = a[i] - '0';
            uint32_t const bj = b[j] - '0';
            v += ai * bj;
        }
        result.push_back(static_cast<char>(v % 10) + '0');
        r = v / 10;
    }
    for (; r > 0; r /= 10)
    {
        result.push_back(static_cast<char>(r % 10) + '0');
    }
}

int
main()
{
    cout << "calcolo di 12345^1000 (versione string): ";
    string a = "1";
    string b = "54321";
    auto start1 = high_resolution_clock::now();
    string result;
    result.reserve(5010);
    a.reserve(5010);
    for (uint32_t i = 0; i < 1000; ++i)
    {
        multiplication(result, a, b);
        swap(result, a);
    }
    auto stop1 = high_resolution_clock::now();
    auto duration1 = duration_cast<milliseconds>(stop1 - start1);
    cout << duration1.count() << " ms" << endl;
    //-----------------------------------------------------------
    cout << "calcolo di 12345^1000 (versione vector): ";
    vector<uint8_t> c = {1};
    vector<uint8_t> d = {5, 4, 3, 2, 1};
    auto start2 = high_resolution_clock::now();
    vector<uint8_t> resultv;
    resultv.reserve(5010);
    c.reserve(5010);
    for (unsigned int i = 0; i < 1000; ++i)
    {
        multiplication(resultv, c, d);
    }
    auto stop2 = high_resolution_clock::now();
    auto duration2 = duration_cast<milliseconds>(stop2 - start2);
    cout << duration2.count() << " ms" << endl;
}


La versione con i vector domina di molto in questa versione.

Nota che le versioni professionali di librerie di questo tipo usano algoritmi ancora migliori (per lo meno quanto i numeri diventano grandi) e utilizzano l'approccio di apatriarca. Io ho usato la versione con gli uint8_t per mostrarti che il tuo codice è centinaia di volte peggio di un codice performante. Raggruppare in gruppi da 9 cifre dovrebbe migliorare le performance di 9 volte. Usare potenze di due è ancora meglio, ma rende meno efficiente il passaggio da e per string. Quindi la scelta su che approccio usare dipende da quanto ti serva convertire da e per string.
vict85
Moderatore
Moderatore
 
Messaggio: 10541 di 19253
Iscritto il: 16/01/2008, 00:13
Località: Berlin

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

Messaggioda ghira » 25/11/2021, 17:48

A parte tutto questo, stiamo veramente calcolando $a^1000$ moltiplicando per $a$ mille volte?

In qualunque modo facciamo le moltiplicazioni, ne stiamo facendo troppissime. O non ho capito niente?
Avatar utente
ghira
Cannot live without
Cannot live without
 
Messaggio: 1144 di 3914
Iscritto il: 11/09/2019, 09:36

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

Messaggioda apatriarca » 25/11/2021, 17:53

@ghira: il calcolo non è importante perché lo scopo era quello di confrontare le performance di due codici che fanno la stessa cosa.
apatriarca
Moderatore
Moderatore
 
Messaggio: 5632 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid

PrecedenteProssimo

Torna a Informatica

Chi c’è in linea

Visitano il forum: Nessuno e 1 ospite