C++ - Ricerca numeri primi

Pubblicità

M1n021

Utente Attivo
Messaggi
194
Reazioni
84
Punteggio
41
Ciao a tutti, sulla scia di un recente topic ho deciso di scrivere una funzione next_prime_number(n) in C++.

Per la precisione ho provato ad implementare una sorta di crivello di Eratostene "espandibile". Partendo dal vettore v = {2,3} aggiungo N valori dispari, applico il crivello partendo dal 3 (visto che numeri pari non ce ne sono) e aggiorno v in modo che contenga solo numeri primi. Per trovare il numero primo immediatamente successivo ad n , applico la suddetta procedura fin quando l'ultimo elemento di v non è maggiore di n . A questo punto cerco il valore voluto all'interno di v dimezzando di volta in volta l'intervallo di ricerca.

A livello logico la parte più complicata è stata, nelle "espansioni" successive alla prima, l'individuazione del primo multiplo da eliminare in relazione al generico numero primo considerato. Faccio un esempio per chiarire quello che intendo... questa è la situazione di v dopo la seconda espansione, ma prima della seconda crivellatura:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103
107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
205
207 209 211 213 215 217 219 221 223 225 227 229 231 233 235 237 239 241 243 245
247 249 251 253 255 257 259 261 263 265 267 269 271 273 275 277 279 281 283 285
287 289 291 293 295 297 299 301 303 305 307 309 311 313 315 317 319 321 323 325
327 329 331 333 335 337 339 341 343 345 347 349 351 353 355 357 359 361 363 365
367 369 371 373 375 377 379 381 383 385 387 389 391 393 395 397 399 401 403
I numeri in rosso sono i numeri primi trovati dopo la prima crivellatura, mentre gli altri sono quelli da sottoporre alla seconda crivellatura. La difficoltà di cui parlavo è consistita nel trovare un modo efficiente per individuare l'indice relativo al primo multiplo da eliminare per il generico numero primo considerato. Per esempio considerando il 7, da dove parto? E considerando il 13 o il 17? A tal proposito, se trovate un modo più efficiente di quello che ho implementato, sono tutto orecchi.

Ecco il codice e l'output relativo ai valori inseriti:
C++:
#include <iostream>
#include <cstdint>
#include <vector>

using namespace std;

void espandi_e_applica_crivello(vector<uint64_t> &v, uint64_t &last, const uint64_t N)
{
    uint64_t m = v.size();
    uint64_t first = last + 2;
    v.reserve(m + N);
    for(uint64_t i = 0; i < N; v.push_back(last += 2), ++i);
    for(uint64_t i = 1, n, q, k; ; ++i)
    {
        if(n = v[i])
        {
            if((q = n * n) > last)
            {
                break;
            }
            if(q < first)
            {
                uint64_t r = first % n;
                k = r ? (r % 2 ? (n - r) / 2 : n - r / 2) : 0;
            }
            else
            {
                k = (q - first) / 2;
            }
            for(uint64_t j = m + k; j < v.size(); v[j] = 0, j += n);
        }
    }
    for(uint64_t i = m; i < v.size(); ++i)
    {
        if(v[i])
        {
            v[m++] = v[i];
        }
    }
    v.resize(m);
}

uint64_t next_prime_number(const uint64_t n)
{
    static vector<uint64_t> v = {2, 3};
    static uint64_t last = v.back();
    static const uint64_t N = 1'000'000;
    while(v.back() <= n)
    {
        espandi_e_applica_crivello(v, last, N);
    }

    cout << "\n" << v.size() << " NUMERI PRIMI <= " << last << " (" << (double)v.size() / last * 100 << "%)\n";

    uint64_t i_dx = v.size() - 1;
    for(uint64_t i_sx = 0, i; i_dx != i_sx + 1; (n < v[i = (i_sx + i_dx) / 2] ? i_dx : i_sx) = i);
    return v[i_dx];
}

int main()
{
    uint64_t n = 47'306'346;

    uint64_t p = next_prime_number(n);
    cout << "\nNEXT PRIME(" << n << ") = " << p << endl;
}
Codice:
2888144 NUMERI PRIMI <= 48000003 (6.01697%)

NEXT PRIME(47306346) = 47306351

Process returned 0 (0x0)   execution time : 1.698 s
Press any key to continue.

Prima di complicare le cose con i big_int , ho preferito limitarmi all'utilizzo degli uint64_t .

Al momento ho impostato N = 1.000.000 , consigli su come settare tale variabile al fine di ottimizzare le prestazioni?

Infine volevo qualche parere e/o consiglio sull'idea di base e su come l'ho implementata. Secondo voi è un buon approccio per una funzione next_prime per input molto grandi? O conviene utilizzare una funzione che verifica la primalità di un singolo numero alla volta?
 
Ciao,

risposta un pò rapida, cerco di guardare meglio stasera o domani.

Non so quanto guadagni, dipende da quanti numeri inserisci nel vettore. Ti conviene inizializzare il vector con già un tot di elementi. Com'è ora al raggiungimento del limite avviene una nuova allocazione di memoria per memorizzare gli altri valori.

Puoi provare con un array non allocato dinamicamente, visto che conosci già la dimensione (N), ma non so quanto vai a guadagnare (a quel punto puoi usare C 🤣).

Ci sarebbero magari altre finezze, ma dovrei mettermi con calma per capire se ha senso (magari facendo prove). Per dire, puoi minimizzare il cache-miss usando il prefetching sfruttando il compilatore https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fprefetch

I tempi che stai ottenendo ora, sono compilando con -O2?
 
Non so quanto guadagni, dipende da quanti numeri inserisci nel vettore. Ti conviene inizializzare il vector con già un tot di elementi. Com'è ora al raggiungimento del limite avviene una nuova allocazione di memoria per memorizzare gli altri valori.

Puoi provare con un array non allocato dinamicamente, visto che conosci già la dimensione (N), ma non so quanto vai a guadagnare (a quel punto puoi usare C 🤣).
L'idea di fondo è quella di implementare un crivello "dinamico" per evitare un eccessivo consumo di memoria e problemi ad esso legati. Per esempio se fisso N ad 1 miliardo, andrei ad utilizzare circa 4GB di memoria (considerando solo i numeri dispari). Mentre dinamicamente, visto che i numeri primi minori di 1 miliardo sono circa 50.000.000, fissando N ad 1 milione il consumo massimo di memoria dovrebbe aggirarsi sui 400MB. Inoltre ogni chiamata a espandi_e_applica_crivello() non richiede sempre una reallocazione di v ; le reallocazioni infatti saranno più frequenti all'inizio per poi diminuire col diradarsi dei numeri primi al crescere dei valori; per esempio volendo trovare i numeri primi fino ad un miliardo e fissando N ad un milione, le reallocazioni di v dovrebbero essere solo 50, al netto invece di 500 chiamate a espandi_e_applica_crivello() .

I tempi che stai ottenendo ora, sono compilando con -O2?
Con -O3, ma tieni presente che qui sul forum mi hanno preso tutti in giro sulle specifiche del mio PC! 😂
 
k = r ? (r % 2 ? (n - r) / 2 : n - r / 2) : 0;
nell'ottica di rendere ancora più illeggibile il codice 😆 invece di fare la divisione per 2 fai uno shift verso destra di un bit con riempimento di zeri a sinistra (aumenta l'efficienza: spostare verso destra di un bit un intero equivale a dividerlo per 2 ma si fa in meno tempo macchina rispetto alla divisione)

scherzi a parte
l'individuazione del primo multiplo da eliminare in relazione al generico numero primo considerato
...
La difficoltà di cui parlavo è consistita nel trovare un modo efficiente per individuare l'indice relativo al primo multiplo da eliminare per il generico numero primo considerato
a naso non vedo un buon modo di farlo, i primi non hanno una regolarità e di conseguenza non ce l'hanno gli interi relativi agli indici del vettore di dispari da crivellare
se c'è mi piacerebbe conoscerlo (per cultura personale)
Secondo voi è un buon approccio per una funzione next_prime per input molto grandi?
dipende da cosa intendi per "molto grandi", anche qui mi sa di no
su un PC da casa e limitandosi a interi a 32 bit i tempi di calcolo potrebbero essere ragionevoli e bastare la memoria
se non ho capito male usi un vettore dinamico per immagazzinare i risultati, ma i numeri in gioco sono un po' troppi, per esempio il numero di primi <= 2^64 - 1 (massimo intero non negativo rappresentabile con 64 bit "unsigned" è di circa 4.15829×10^17), un po' troppo per entrare in un database in RAM (se non ho fatto male i conti ci vogliono circa 3 miliadi di Gibibyte = gigabyte binari)

temo che qui ci voglia un matematico professionista specializzato in teoria dei numeri per dare suggerimenti determinanti
 
L'idea di fondo è quella di implementare un crivello "dinamico" per evitare un eccessivo consumo di memoria e problemi ad esso legati. Per esempio se fisso N ad 1 miliardo, andrei ad utilizzare circa 4GB di memoria (considerando solo i numeri dispari). Mentre dinamicamente, visto che i numeri primi minori di 1 miliardo sono circa 50.000.000, fissando N ad 1 milione il consumo massimo di memoria dovrebbe aggirarsi sui 400MB. Inoltre ogni chiamata a espandi_e_applica_crivello() non richiede sempre una reallocazione di v ; le reallocazioni infatti saranno più frequenti all'inizio per poi diminuire col diradarsi dei numeri primi al crescere dei valori; per esempio volendo trovare i numeri primi fino ad un miliardo e fissando N ad un milione, le reallocazioni di v dovrebbero essere solo 50, al netto invece di 500 chiamate a espandi_e_applica_crivello() .

Ah sorry, mi ero completamente perso la call a "reserve"; stai già facendo ciò che proponevo.

Il passo successivo potrebbero essere indicare inline quella funzione comunque.

Puoi anche valutare l'utilizzo delle intrinsics, ma sinceramente non so quanto guadagneresti... Se provi con tipi più grandi di 64bit, magari il compilatore fa già uso di SSE2 / AVX-256 / AVX-512 inoltre. Al posto tuo proverei; se non fa uso di questi set, avresti un margine di miglioramento con le intrnsics magari.

Purtroppo quello che fai non è parallelizzabile, altrimenti con qualche thread avresti altro guadagno.

(Puoi riscrivere tutto per usare CUDA... 🤣)

Con -O3, ma tieni presente che qui sul forum mi hanno preso tutti in giro sulle specifiche del mio PC! 😂

Si bhe, l'HW allora è un limite lol

nell'ottica di rendere ancora più illeggibile il codice 😆 invece di fare la divisione per 2 fai uno shift verso destra di un bit con riempimento di zeri a sinistra (aumenta l'efficienza: spostare verso destra di un bit un intero equivale a dividerlo per 2 ma si fa in meno tempo macchina rispetto alla divisione)

scherzi a parte

C++:
 k = (q - first) / 2;

Codice:
        sub     rax, r14
        shr     rax

Con -O2 provando online, vedo che sta già avvenendo... 😜😜
 
Con -O2 provando online, vedo che sta già avvenendo.
sì ma con " / 2 " il codice è solo molto scarsamente leggibile, l'obiettivo è renderlo il più illeggibile possibile, in modo che sia impossibile per chiunque capire come è implementato l'algoritmo 🤣

seriamente, qui il problema non è il codice, sono sicurissimo che fa esattamente cosa dice
ma per questo tipo di problemi, per tagliare i tempi di calcolo servono algoritmi scritti da qualcuno con alle spalle una conoscenza teorica profonda e specialistica
Toccherà leggerere qualche buon testo di Teoria dei Numeri, almeno quella elementare
 
Toccherà leggerere qualche buon testo di Teoria dei Numeri, almeno quella elementare

Sono sicuro che nei testi di Knuth qualcosa ci sarà... 😁

Concordo comunque. Visto il tipo di problema anche se si recupera qualcosa con queste ottimizzazioni sarà comunque poca cosa, probabilmente.
 
nell'ottica di rendere ancora più illeggibile il codice 😆 invece di fare la divisione per 2 fai uno shift verso destra di un bit con riempimento di zeri a sinistra (aumenta l'efficienza: spostare verso destra di un bit un intero equivale a dividerlo per 2 ma si fa in meno tempo macchina rispetto alla divisione)
Ho evitato per non rendere il codice troppo criptico, visto che con le ottimizzazioni abilitate dovrebbe essere automatico con i tipi interi nativi, invece nel momento in cui utilizzassi la libreria sui big_int che scrissi qualche tempo fa dovrei esplicitarlo (e in tal caso il guadagno non sarebbe cosa da poco)! 😁

a naso non vedo un buon modo di farlo, i primi non hanno una regolarità e di conseguenza non ce l'hanno gli interi relativi agli indici del vettore di dispari da crivellare
se c'è mi piacerebbe conoscerlo (per cultura personale)
Il modo più efficiente che mi è venuto in mente è quello riportato nel codice. Giusto per chiarire i contorni del problema, considera quanto segue:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103
107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
205
207 209 211 213 215 217 219 221 223 225 227 229 231 233 235 237 239 241 243 245
247 249 251 253 255 257 259 261 263 265 267 269 271 273 275 277 279 281 283 285
287 289 291 293 295 297 299 301 303 305 307 309 311 313 315 317 319 321 323 325
327 329 331 333 335 337 339 341 343 345 347 349 351 353 355 357 359 361 363 365
367 369 371 373 375 377 379 381 383 385 387 389 391 393 395 397 399 401 403
I numeri in rosso sono i numeri primi trovati dopo la prima crivellatura, mentre gli altri sono quelli da sottoporre alla seconda crivellatura.
I numeri da "crivellare" sono quelli in nero, e chiamiamo m l'indice del primo di questa sequenza (ossia di 205).
Nel classico algoritmo del crivello di Eratostene quando si vogliono eliminare i multipli di 17 si parte ovviamente dal suo quadrato ossia 17*17=289, e in questo caso, essendo 289>205, sarà lo stesso anche qui, e quindi non resta che ricavare l'indice associato a 289, che può essere calcolato come m+(289-205)/2.
Nel momento in cui invece vogliamo eliminare i multipli di 7 le cose cambiano, essendo 49<205; in questo caso dobbiamo trovare il primo multiplo dispari di 7 che sia >=205 e ricavare poi il suo indice nell'array. Ed è qui che entra in gioco:
C++:
k = r ? (r % 2 ? (n - r) / 2 : n - r / 2) : 0;
In pratica mi sono reso conto che il resto di numeri dispari successivi per un determinato numero dispari a genera una sequenza ciclica costituita prima dai numeri pari e poi dai numeri dispari minori di a . Per esempio nel caso di a=7 avremo

0 2 4 6 1 3 5 0 ...

mentre per a=19 avremo

0 2 4 6 8 10 12 14 16 18 1 3 5 7 9 11 13 15 17 0 ...

E la formula "illeggibile" 😆 di cui sopra, non fa altro che sfruttare questa proprietà.

dipende da cosa intendi per "molto grandi", anche qui mi sa di no
su un PC da casa e limitandosi a interi a 32 bit i tempi di calcolo potrebbero essere ragionevoli e bastare la memoria
se non ho capito male usi un vettore dinamico per immagazzinare i risultati, ma i numeri in gioco sono un po' troppi, per esempio il numero di primi <= 2^64 - 1 (massimo intero non negativo rappresentabile con 64 bit "unsigned" è di circa 4.15829×10^17), un po' troppo per entrare in un database in RAM (se non ho fatto male i conti ci vogliono circa 3 miliadi di Gibibyte = gigabyte binari)
In effetti già restando nell'ambito degli uint64_t non risulta fattibile immagazzinare tutti i numeri primi, quindi bisogna comunque provvedere ad una funzione che verifica la primalità di un singolo numero alla volta, e a tal proposito la cosa più semplice che mi viene in mente è sfruttare un file (ricavato per esempio col suddetto codice) contenente i primi tot numeri primi, per poi testare come divisori i restanti numeri dispari fino alla radice intera del numero.

Concordo comunque. Visto il tipo di problema anche se si recupera qualcosa con queste ottimizzazioni sarà comunque poca cosa, probabilmente.
Lo penso anche io. Al momento il massimo che mi viene in mente per una funzione next_prime_number(n) (eventualmente anche per big_int) è quello che ho appena scritto sopra.
 
ingegnoso!
il motivo dovrebbe risiedere nell'aritmentica modulare e mi viene il dubbio che sfruttandola si possa far meglio, magari senza entrare in un ciclo, sarebbe da studiarci
In pratica mi sono reso conto che il resto di numeri dispari successivi per un determinato numero dispari a genera una sequenza ciclica costituita prima dai numeri pari e poi dai numeri dispari minori
questo perché il resto r è 0<=r<d (dove d è un divisore dispari di un numero dispari), partendo da un multiplo di d il primo resto della divisione è 0, poi 2 (perché il dispari successivo ha 2 unità in più) e così via fino ad arrivare a d-1, dopodiché il successivo dispari fa aumentare il quoziente di una unità e la successione dei resti sarà 1, 3, 5.. fino a d-2, poi il ciclo ricomincia da 0 perché arrivi al multiplo successivo
 
A quale ciclo ti riferisci?
a quello un paio di parentesi più sopra
for(uint64_t i = 1, n, q, k; ; ++i)
oppure l'ho mal interpretato ed è il ciclo che scandisce i primi già trovati uno ad uno ma poi il calcolo dell'indice è un'unica istruzione, quella con l'operatore ternario (espressione condizionale) ?
 
oppure l'ho mal interpretato ed è il ciclo che scandisce i primi già trovati uno ad uno
Esatto, è quello che scandisce i primi già trovati al fine di eliminarne i multipli.

ma poi il calcolo dell'indice è un'unica istruzione, quella con l'operatore ternario (espressione condizionale) ?
Sì, l'espressione con l'operatore ternario calcola l'incremento k dell'indice di first nel caso q < first .

Ho dato un'occhiata qui


e mi sembra di capire che alla fine l'unico test di primalità "esatto" è quello che controlla la divisibilità del numero per tutti i primi minori o uguali della sua radice quadrata intera; in realtà esistono alcune varianti ma hanno una complessità sovrapponibile, se non superiore, al suddetto metodo.
Esiste poi tutta una galassia di metodi euristici (non provati) o su base probabilistica; quindi mi sa che alla fine la verifica pratica di primalità per interi molto grandi sia attualmente un problema irrisolto.

E da quanto ho potuto capire anche la funzione python sympy.nextprime(n) utilizzata dall'OP in questo topic è su base probabilistica per
n>2^64-1 (il che aggiunge ulteriore aleatorietà all'algoritmo da lui presentato 😅).
 
E da quanto ho potuto capire anche la funzione python sympy.nextprime(n) utilizzata dall'OP in questo topic è su base probabilistica per
n&gt;2^64-1 (il che aggiunge ulteriore aleatorietà all'algoritmo da lui presentato 😅).

Intervengo solo per porre una domanda e non voglio avviare nessuna polemica con nessuno. Immagino che questo messaggio si riferisca a me e per tanto ti chiedo: Stai affermando che il mal funzionamento del mio algoritmo si basa sul fatto che i numeri trattati non sono primi perchè vengono trovati con un algoritmo probabilistico? Solo si o no per favore. Poi chiudo qui
 
mi sembra di capire che alla fine l'unico test di primalità "esatto" è quello che controlla la divisibilità del numero per tutti i primi minori o uguali della sua radice quadrata intera
sì, purtroppo è così, non si può essere mai sicuri al 100% fin quando non siano state provate tutte le divisioni
gli algoritmi probabilistici sono molto precisi nel senso che più sono avanzati minore è la probabilità che giudichino primo un numero che in realtà non lo è;
la verifica pratica di primalità per interi molto grandi sia attualmente un problema irrisolto
primalità, fattorizzazione e così via sono tutti problemi semplicissima da capire, perfino da risolvere a mano quando i numeri sono piccoli; ma al crescere del numero di cifre aumenta in modo esponenziale il numero di operazioni da eseguire, il che richiede tempi non alla portata di computer classici
Stai affermando che il mal funzionamento del mio algoritmo si basa sul fatto che i numeri trattati non sono primi perchè vengono trovati con un algoritmo probabilistico? Solo si o no per favore.
non ho scritto io il post ma so rispondere ugualmente: la risposta è NO, non è quello il motivo, sono le affermazioni che lo contornano a essere sbagliate, in più sono matematicamente sbagliate le sole 3 istruzioni che secondo te risolvono una fattorizzazione. E chiudiamola qui perché si va off-topic.
 
Intervengo solo per porre una domanda e non voglio avviare nessuna polemica con nessuno. Immagino che questo messaggio si riferisca a me e per tanto ti chiedo: Stai affermando che il mal funzionamento del mio algoritmo si basa sul fatto che i numeri trattati non sono primi perchè vengono trovati con un algoritmo probabilistico? Solo si o no per favore. Poi chiudo qui
Non ho mai parlato di malfunzionamento, ma se ad una proprietà (quella che i semiprimi, ottenuti dalla moltiplicazione di due generici primi appartenenti a due determinati intervalli, siano tutti divisibili per un determinato valore da te chiamato chiave) matematicamente non dimostrata aggiungi il fatto che i primi sono ottenuti con un metodo su base probabilistica, non mi sembra che il termine "aleatorio" sia così campato in aria.
E comunque non c'è bisogno di prendersela, lo sai che mi sto impegnando nel cercare di venire matematicamente a capo del problema. Infatti l'argomento di questo topic nasce proprio dal fatto di voler testare il tuo algoritmo, ma non conoscendo il python sto cercando di implementare una funzione next_prime_number() in C++, da associare poi alla libreria sui big_int che ho scritto qualche tempo fa.
 
Ultima modifica:
Pubblicità
Pubblicità
Indietro
Top