Si vous avez trouvé ce site intéressant, qu'il vous a appris 2-3 astuces, voir qu'il vous a fait rire ou pleurer (euh ?). N'hésitez pas à le soutenir en nous envoyant des coins à l'une des 3 adresses suivantes, merci. Adresse pour les Bitcoins Adresse pour les Litecoins Adresse pour les Dodgecoins Le crible d'AtkinLe principe
Le crible d'Atkin est une méthode considérée comme une version optimisée
du crible d'Eratosthène pour trouver tous les nombres premiers jusqu'à
une borne fixée N. L'algorithme a été conçu récemment par A. O. L. Atkin et
Daniel J. Bernstein dont vous trouverez la publication de leurs travaux "Prime
sieves using binary quadratic forms" en suivant ce
lien.
Première implémentationD'un point de vue algorithmique, commençons par implémenter la version trouvée en pseudo code dans le Wikipédia anglais histoire de comprendre comment ça fonctionne. Malgré le problème de contenu cité ci-dessus, le site semble être un bon point de départ pour appréhender Atkin.
private static final int MAX = 100000000; private static final int SQRT_MAX = (int) Math.sqrt(MAX) + 1; private static boolean[] array = new boolean[MAX]; //--// for (int x = 1; x < SQRT_MAX; x++) { for (int y = 1; y < SQRT_MAX; y++) { int k = 4 * x * x + y * y; if ((k < MAX) && ((k % 12 == 1) || (k % 12 == 5))) { array[k] = !array[k]; } k = 3 * x * x + y * y; if ((k < MAX) && (k % 12 == 7)) { array[k] = !array[k]; } if (x > y) { k = 3 * x * x - y * y; if ((k < MAX) && (k % 12 == 11)) { array[k] = !array[k]; } } } } array[2] = true; array[3] = true; for (int n = 5; n <= SQRT_MAX; n++) { if (array[n]) { int n2 = n * n; for (int k = n2; k < MAX; k += n2) { array[k] = false; } } }
Au niveau des performances nous avons un petit espoir car les temps d'un
Atkin non optimisé sont meilleurs que les temps d'un Eratosthène 1er du nom
non optimisé puisque nous avons 6 secondes 9 pour Atkin contre 7 secondes 5 pour
Eratosthène (dans le cas des premiers 100 millions).
Implémentation, analyse des temps
Pour pouvoir optimiser Atkin, essayons d'analyser le programme et voir
où il prend le plus de temps, pour cela nous allons d'abord couper la première
boucle en 3 afin de pouvoir calculer les temps intermédiaires. Bien entendu
l'éclatement de cette boucle n'influence pas le résultat, nous avons bien tous
les premiers à la fin de l'exécution du programme.
double debut = System.currentTimeMillis();
int xUpper = (int) Math.sqrt(MAX / 4) + 1; for (int x = 1; x < xUpper; x++) { for (int y = 1; y < Math.sqrt(MAX - k1); y++) { Si (4x² + y²) % 12 == 1 ou (4x² + y²) % 12 == 5 --> Alors array[k] = !array[k]; } } double intermediaire1 = System.currentTimeMillis(); xUpper = (int) Math.sqrt(MAX / 3) + 1; for (int x = 1; x < xUpper; x++) { for (int y = 2; y < Math.sqrt(MAX - k1); y++) { Si (3x² + y²) % 12 == 7 Alors array[k] = !array[k]; } } double intermediaire2 = System.currentTimeMillis(); xUpper = (int) Math.sqrt(MAX); for (int x = 1; x < xUpper; x++) { for (int y = 1; y < x; y++) { Si (3x² - y²) % 12 == 11 Alors array[k] = !array[k]; } } double intermediaire3 = System.currentTimeMillis(); for (int n = 5; n <= SQRT_MAX; n++) { Elimination des multiples des carrés n², 2n², 3n², ..., MAX } double fin = System.currentTimeMillis();
Nous voyons que le temps de la boucle 1 est supérieur à la boucle 2, lui-même
supérieur à la boucle 3 et lui-même supérieur à la boucle d'élimination des
multiples des carrés. De plus la boucle 1 prend 43% du temps total d'exécution
de l'algorithme, la boucle 2 33%, la boucle 3 15% et la boucle des carrées 7%. Deuxième implémentation
Pour optimiser l'algorithme, nous allons nous attaquer à l'opérateur
modulo qui représente en informatique une opération couteuse en temps.
int k1 = 3 * x * x; for (int y = 2; y < Math.sqrt(MAX - k1); y++) { int k = k1 + y * y; if (k % 12 == 7) { System.out.println("x = " + x + " y = " + y); } } } x = 55 y = 9988 x = 55 y = 9992 x = 55 y = 9994 x = 55 y = 9998 x = 57 y = 2 x = 57 y = 4 x = 57 y = 8 x = 57 y = 10 x = 57 y = 14 Nous voyons que les solutions répondent aux règles suivantes :
int[] sequence2 = { 4, 2 }; for (int x = 1; x < xUpper; x += 2) { int index = 0; int k1 = 3 * x * x; for (int y = 2; y < Math.sqrt(MAX - k1); y += sequence2[(++index & 1)]) { int k = k1 + y * y; array[k] = !array[k]; } } Et les améliorations sont au rendez-vous. source
L'optimisation donne de bon résultat puisque la boucle est 5 fois plus rapide que la précédente implémentation mais ne vous emballez pas car cette boucle se prêtait bien à cette modification... Troisième implémentation
Maintenant que nous avons vu comment améliorer les temps de la boucle 2,
nous pouvons faire de même sur les 2 autres boucles.
Enjoy ! Nous avons réussi à obtenir des meilleurs temps qu'un Eratosthène
optimisé. A titre de comparaison, l'Eratosthène "deuxième implémentation"
est à 3.063 secondes pour une borne à 100 millions alors qu'Atkin est
à 2.187, nous avons donc un gain d'un peu moins de 50%.
source Quatrième implémentation
Continuons sur la lancé et essayons d'atteindre le milliard en implémentant
Atkin avec un tableau de bit, c'est exactement la même approche que nous avions
eu avec la 3ème implémentation d'Erastotene.
Nous aurons besoin de 2 opérations de bit supplémentaires par rapport à
Erastotene à savoir : unSetBit permettant de mettre à 0 un bit du tableau et
toggleBit permettant de switcher un bit.
source
Et la c'est le drame, Atkin devient plus long ! Mais pourquoi ??
En conclusion
Nous voyons qu'Atkin peut aller plus vite qu'Eratosthène lorsqu'il est
"savamment" optimisé mais que d'effort pour arriver à ce résultat. |