Merge pull request 'Wiki in English, Danish and Malay, AVX2 explanation, README' (#3) from docs/wiki-and-dist-build into main
Publish wiki / publish-wiki (push) Successful in 19s
Publish wiki / wiki (push) Successful in 19s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s

This commit was merged in pull request #3.
This commit is contained in:
2026-10-19 19:42:17 +02:00
17 changed files with 487 additions and 5 deletions
+24
View File
@@ -0,0 +1,24 @@
# Byg og kør
🌐 [English](Build-and-Run) · **Dansk** · [Bahasa Melayu](Build-and-Run-ms)
`Makefile` ligger i repositoriets rod og lægger programmet i `dist/`. Du skal
bruge `gcc` (Linux, eller MinGW-w64 / MSYS2 på Windows) og en CPU med AVX2.
```bash
make
```
```bash
make run
```
```bash
make clean
```
Sådan gemmer du primtallene og stadig ser tiden: omdiriger kun standard output.
```bash
./dist/prime_avx2 > primes.txt
```
+26
View File
@@ -0,0 +1,26 @@
# Bina dan jalankan
🌐 [English](Build-and-Run) · [Dansk](Build-and-Run-da) · **Bahasa Melayu**
`Makefile` berada di akar repositori dan meletakkan fail boleh laksana dalam
`dist/`. Anda memerlukan `gcc` (Linux, atau MinGW-w64 / MSYS2 pada Windows) dan
CPU dengan AVX2.
```bash
make
```
```bash
make run
```
```bash
make clean
```
Untuk menyimpan nombor perdana dan masih melihat masa, alihkan standard output
sahaja:
```bash
./dist/prime_avx2 > primes.txt
```
+1 -1
View File
@@ -1,6 +1,6 @@
# Build and run
**English**
🌐 **English** · [Dansk](Build-and-Run-da) · [Bahasa Melayu](Build-and-Run-ms)
The `Makefile` is in the repository root and puts the executable in `dist/`.
It needs `gcc` (Linux, or MinGW-w64 / MSYS2 on Windows) and a CPU with AVX2.
+34
View File
@@ -0,0 +1,34 @@
# Beskrivelse
🌐 [English](Description) · **Dansk** · [Bahasa Melayu](Description-ms)
## Formål
Programmet viser, hvor hurtig en CPU er. Hvor hurtig den ser ud til at være,
afhænger af, hvordan programmet er skrevet, så tallet måler CPU'en og koden
sammen, ikke CPU'en alene.
## Hvad det gør
Programmet tester hvert tal fra 0 til 1.000.000 med `is_prime_avx2`, som bruger
AVX2-prøvedivision med 6k ± 1 (fire divisorer pr. trin). Det udskriver hvert
primtal på sin egen linje og skriver en opsummering til standard error:
```text
Found 78498 primes from 0 to 1000000 in 13.806 ms
```
78.498 er det kendte antal primtal op til 1.000.000, så opsummeringen virker
også som kontrol af, at resultatet er rigtigt.
## Hvorfor tiden afhænger af koden
Den samme CPU giver meget forskellige tider for forskellige programmer:
- algoritmen (prøvedivision, en si, en segmenteret si),
- instruktionssættet (skalar kode, AVX2, AVX-512),
- compiler og flag (`-O3 -mavx2`, MSVC `/O2 /arch:AVX2`),
- hvad der måles (kun søgningen, eller også udskrivning til en konsol).
Her måles kun søgningen. Resultatet samles først og udskrives bagefter, så
konsollens hastighed ikke påvirker tallet.
+35
View File
@@ -0,0 +1,35 @@
# Penerangan
🌐 [English](Description) · [Dansk](Description-da) · **Bahasa Melayu**
## Tujuan
Program ini menunjukkan betapa lajunya sesebuah CPU. Kelajuan yang kelihatan
bergantung pada cara program ditulis, jadi angka itu mengukur CPU dan kod
bersama-sama, bukan CPU sahaja.
## Apa yang dilakukannya
Program menguji setiap nombor dari 0 hingga 1,000,000 dengan `is_prime_avx2`,
yang menggunakan pembahagian cuba-cuba AVX2 dengan 6k ± 1 (empat pembahagi
setiap langkah). Ia mencetak setiap nombor perdana pada barisnya sendiri dan
menulis baris ringkasan ke standard error:
```text
Found 78498 primes from 0 to 1000000 in 13.806 ms
```
78,498 ialah bilangan nombor perdana yang diketahui hingga 1,000,000, jadi baris
ringkasan juga berfungsi sebagai semakan ketepatan.
## Mengapa masa bergantung pada kod
CPU yang sama memberi masa yang sangat berbeza untuk program yang berbeza:
- algoritma (pembahagian cuba-cuba, ayak, ayak bersegmen),
- set arahan yang digunakan (kod skalar, AVX2, AVX-512),
- pengkompil dan bendera (`-O3 -mavx2`, MSVC `/O2 /arch:AVX2`),
- apa yang diukur (carian sahaja, atau juga pencetakan ke konsol).
Di sini hanya carian diukur. Keputusan dikumpul dahulu dan dicetak kemudian,
supaya kelajuan konsol tidak mengubah angka.
+1 -1
View File
@@ -1,6 +1,6 @@
# Description
**English**
🌐 **English** · [Dansk](Description-da) · [Bahasa Melayu](Description-ms)
## Purpose
+11
View File
@@ -0,0 +1,11 @@
# prime-numbers-c
🌐 [English](Home) · **Dansk** · [Bahasa Melayu](Home-ms)
Et lille C-program, der udskriver alle primtal fra 0 til 1.000.000 og viser,
hvor lang tid det tog.
- [Beskrivelse](Description-da): hvad programmet skal bruges til
- [Sådan virker AVX2](How-AVX2-Works-da): koden, trin for trin
- [Resultater](Results-da): målte tider på rigtig hardware
- [Byg og kør](Build-and-Run-da): `make`, `make run`
+11
View File
@@ -0,0 +1,11 @@
# prime-numbers-c
🌐 [English](Home) · [Dansk](Home-da) · **Bahasa Melayu**
Program C kecil yang menyenaraikan semua nombor perdana dari 0 hingga 1,000,000
dan melaporkan berapa lama ia mengambil masa.
- [Penerangan](Description-ms): kegunaan program ini
- [Cara AVX2 berfungsi](How-AVX2-Works-ms): kod, langkah demi langkah
- [Keputusan](Results-ms): masa yang diukur pada perkakasan sebenar
- [Bina dan jalankan](Build-and-Run-ms): `make`, `make run`
+2 -1
View File
@@ -1,10 +1,11 @@
# prime-numbers-c
**English**
🌐 **English** · [Dansk](Home-da) · [Bahasa Melayu](Home-ms)
A small C program that lists every prime number from 0 to 1,000,000 and
reports how long it took.
- [Description](Description): what the program is for
- [How AVX2 works](How-AVX2-Works): the code, step by step
- [Results](Results): measured times on real hardware
- [Build and run](Build-and-Run): `make`, `make run`
+96
View File
@@ -0,0 +1,96 @@
# Sådan virker AVX2
🌐 [English](How-AVX2-Works) · **Dansk** · [Bahasa Melayu](How-AVX2-Works-ms)
AVX2 er et sæt CPU-instruktioner, der udfører den samme operation på flere tal
på én gang (SIMD: én instruktion, flere data). `is_prime_avx2` bruger det til at
teste **fire divisorer pr. trin** i stedet for én.
## Idéen
Et tal `n` er et primtal, hvis intet tal fra 2 til sqrt(n) går op i det. To
trin mindsker arbejdet:
1. **Spring de fleste kandidater over.** Hvert primtal over 3 har formen 6k - 1
eller 6k + 1 (5, 7, 11, 13, 17, 19, 23, 25, ...). Lige tal og multipla af 3
afvises før løkken, så kun disse divisorer prøves.
2. **Test fire ad gangen.** Ét 256-bit AVX2-register rummer fire `double`-værdier,
så én division, én multiplikation og én sammenligning klarer fire divisorer.
Hver gennemgang af løkken tester divisorerne `d, d+2, d+6, d+8` og går derefter
12 frem: `5,7,11,13` så `17,19,23,25` så `29,31,35,37` og så videre.
## Koden, trin for trin
Kommentarerne i koden er på engelsk, ligesom koden selv.
```c
// Only divisors up to sqrt(n) need testing: a factor above it pairs with one below it.
const double limit = floor(sqrt((double)n));
// Broadcast n and the limit into all 4 double lanes of a 256-bit register.
__m256d v_n = _mm256_set1_pd((double)n);
__m256d v_limit = _mm256_set1_pd(limit);
// Divisors 5, 7, 11, 13 (6k +/- 1); each iteration advances by 12.
// Kept as 4 x int32 in a 128-bit register, converted to double each pass.
__m128i v_div_i = _mm_setr_epi32(5, 7, 11, 13);
const __m128i v_step = _mm_set1_epi32(12);
// base is the first divisor of the group; stop when it passes sqrt(n).
for (double base = 5.0; base <= limit; base += 12.0) {
// Convert the 4 int32 divisors to 4 doubles.
__m256d v_d = _mm256_cvtepi32_pd(v_div_i);
// q = floor(n / d) for all 4 divisors at once.
__m256d v_q = _mm256_floor_pd(_mm256_div_pd(v_n, v_d));
// rem = n - q * d, which is n mod d.
__m256d v_rem = _mm256_sub_pd(v_n, _mm256_mul_pd(v_q, v_d));
// Lane mask: all bits set where the remainder is 0 (d divides n).
__m256d is_zero = _mm256_cmp_pd(v_rem, _mm256_setzero_pd(), _CMP_EQ_OQ);
// Lane mask: all bits set where the divisor is <= sqrt(n).
__m256d in_range = _mm256_cmp_pd(v_d, v_limit, _CMP_LE_OQ);
// A lane only counts if it divides n and the divisor is <= sqrt(n).
// movemask packs the sign bit of each lane into 4 bits; non-zero = a hit.
if (_mm256_movemask_pd(_mm256_and_pd(is_zero, in_range)) != 0)
return false;
// Move to the next four divisors: 5,7,11,13 -> 17,19,23,25 -> ...
v_div_i = _mm_add_epi32(v_div_i, v_step);
}
// No divisor found up to sqrt(n): n is prime.
```
## Hvorfor `in_range`-masken er nødvendig
Den sidste gruppe kan indeholde divisorer over sqrt(n). Uden masken kunne en
bane melde en "faktor", der ikke er en. For `n = 25` er grænsen 5, så kun bane 0
(divisor 5) tæller, og 25 afvises korrekt. For `n = 29` går intet op i tallet,
og 29 er et primtal.
## Hvorfor `double` og ikke `float` eller heltal
AVX2 har ingen heltalsdivision, så divisionen foregår i flydende tal. En
`double` rummer ethvert 32-bit heltal præcist, og IEEE-division er korrekt
afrundet, så `floor(n / d)` er præcis for `n` op til `UINT32_MAX`. En 32-bit
`float` har kun 24 bits præcision og ville give forkerte kvotienter. Det er også
grunden til, at funktionen kun understøtter `n` op til 4.294.967.295.
## Brugte intrinsics
| Intrinsic | Hvad den gør |
| --- | --- |
| `_mm256_set1_pd` | kopierer én double til alle 4 baner |
| `_mm_setr_epi32` | bygger 4 int32-værdier i rækkefølge |
| `_mm256_cvtepi32_pd` | konverterer 4 int32 til 4 double |
| `_mm256_div_pd` / `_mm256_mul_pd` / `_mm256_sub_pd` | dividerer, multiplicerer, trækker fra i 4 baner |
| `_mm256_floor_pd` | runder 4 doubles ned |
| `_mm256_cmp_pd` | sammenligner 4 baner og giver en maske med enere eller nuller pr. bane |
| `_mm256_and_pd` | bitvis OG af to masker |
| `_mm256_movemask_pd` | samler de 4 banemasker til et 4-bit heltal |
| `_mm_add_epi32` | lægger 4 int32-værdier sammen |
De kommer fra `<immintrin.h>`. Byg med `-mavx2` (gcc/clang) eller `/arch:AVX2`
(MSVC). På en CPU uden AVX2 ville programmet gå ned med en ugyldig instruktion.
+99
View File
@@ -0,0 +1,99 @@
# Cara AVX2 berfungsi
🌐 [English](How-AVX2-Works) · [Dansk](How-AVX2-Works-da) · **Bahasa Melayu**
AVX2 ialah set arahan CPU yang melakukan operasi yang sama pada beberapa nombor
serentak (SIMD: satu arahan, banyak data). `is_prime_avx2` menggunakannya untuk
menguji **empat pembahagi setiap langkah** dan bukan satu.
## Idea
Nombor `n` ialah nombor perdana jika tiada nombor dari 2 hingga sqrt(n) yang
membahaginya. Dua langkah mengurangkan kerja:
1. **Langkau kebanyakan calon.** Setiap nombor perdana melebihi 3 berbentuk
6k - 1 atau 6k + 1 (5, 7, 11, 13, 17, 19, 23, 25, ...). Nombor genap dan
gandaan 3 ditolak sebelum gelung, jadi hanya pembahagi ini dicuba.
2. **Uji empat sekali gus.** Satu daftar AVX2 256-bit memuatkan empat nilai
`double`, jadi satu pembahagian, satu pendaraban dan satu perbandingan
mengendalikan empat pembahagi.
Setiap pusingan gelung menguji pembahagi `d, d+2, d+6, d+8` kemudian maju 12:
`5,7,11,13` kemudian `17,19,23,25` kemudian `29,31,35,37`, dan seterusnya.
## Kod, langkah demi langkah
Komen dalam kod ditulis dalam bahasa Inggeris, seperti kod itu sendiri.
```c
// Only divisors up to sqrt(n) need testing: a factor above it pairs with one below it.
const double limit = floor(sqrt((double)n));
// Broadcast n and the limit into all 4 double lanes of a 256-bit register.
__m256d v_n = _mm256_set1_pd((double)n);
__m256d v_limit = _mm256_set1_pd(limit);
// Divisors 5, 7, 11, 13 (6k +/- 1); each iteration advances by 12.
// Kept as 4 x int32 in a 128-bit register, converted to double each pass.
__m128i v_div_i = _mm_setr_epi32(5, 7, 11, 13);
const __m128i v_step = _mm_set1_epi32(12);
// base is the first divisor of the group; stop when it passes sqrt(n).
for (double base = 5.0; base <= limit; base += 12.0) {
// Convert the 4 int32 divisors to 4 doubles.
__m256d v_d = _mm256_cvtepi32_pd(v_div_i);
// q = floor(n / d) for all 4 divisors at once.
__m256d v_q = _mm256_floor_pd(_mm256_div_pd(v_n, v_d));
// rem = n - q * d, which is n mod d.
__m256d v_rem = _mm256_sub_pd(v_n, _mm256_mul_pd(v_q, v_d));
// Lane mask: all bits set where the remainder is 0 (d divides n).
__m256d is_zero = _mm256_cmp_pd(v_rem, _mm256_setzero_pd(), _CMP_EQ_OQ);
// Lane mask: all bits set where the divisor is <= sqrt(n).
__m256d in_range = _mm256_cmp_pd(v_d, v_limit, _CMP_LE_OQ);
// A lane only counts if it divides n and the divisor is <= sqrt(n).
// movemask packs the sign bit of each lane into 4 bits; non-zero = a hit.
if (_mm256_movemask_pd(_mm256_and_pd(is_zero, in_range)) != 0)
return false;
// Move to the next four divisors: 5,7,11,13 -> 17,19,23,25 -> ...
v_div_i = _mm_add_epi32(v_div_i, v_step);
}
// No divisor found up to sqrt(n): n is prime.
```
## Mengapa topeng `in_range` diperlukan
Kumpulan terakhir boleh mengandungi pembahagi yang melebihi sqrt(n). Tanpa
topeng, satu lorong boleh melaporkan "faktor" yang bukan faktor. Untuk `n = 25`
had ialah 5, jadi hanya lorong 0 (pembahagi 5) dikira dan 25 ditolak dengan
betul. Untuk `n = 29` tiada yang membahagikannya dan 29 ialah nombor perdana.
## Mengapa `double` dan bukan `float` atau integer
AVX2 tiada pembahagian integer, jadi pembahagian dibuat dalam titik terapung.
`double` memuatkan setiap integer 32-bit dengan tepat dan pembahagian IEEE
dibundarkan dengan betul, jadi `floor(n / d)` tepat untuk `n` sehingga
`UINT32_MAX`. `float` 32-bit hanya mempunyai ketepatan 24 bit dan akan memberi
hasil bahagi yang salah. Ini juga sebabnya fungsi ini hanya menyokong `n`
sehingga 4,294,967,295.
## Intrinsic yang digunakan
| Intrinsic | Fungsinya |
| --- | --- |
| `_mm256_set1_pd` | salin satu double ke semua 4 lorong |
| `_mm_setr_epi32` | bina 4 nilai int32 mengikut urutan |
| `_mm256_cvtepi32_pd` | tukar 4 int32 kepada 4 double |
| `_mm256_div_pd` / `_mm256_mul_pd` / `_mm256_sub_pd` | bahagi, darab, tolak dalam 4 lorong |
| `_mm256_floor_pd` | bundarkan 4 double ke bawah |
| `_mm256_cmp_pd` | banding 4 lorong, beri topeng semua-satu atau semua-sifar setiap lorong |
| `_mm256_and_pd` | DAN bitwise bagi dua topeng |
| `_mm256_movemask_pd` | gabungkan 4 topeng lorong menjadi integer 4-bit |
| `_mm_add_epi32` | tambah 4 nilai int32 |
Ia datang daripada `<immintrin.h>`. Bina dengan `-mavx2` (gcc/clang) atau
`/arch:AVX2` (MSVC). Pada CPU tanpa AVX2 program akan ranap dengan arahan tidak
sah.
+95
View File
@@ -0,0 +1,95 @@
# How AVX2 works
🌐 **English** · [Dansk](How-AVX2-Works-da) · [Bahasa Melayu](How-AVX2-Works-ms)
AVX2 is a set of CPU instructions that do the same operation on several
numbers at once (SIMD: single instruction, multiple data). `is_prime_avx2`
uses it to test **four divisors per step** instead of one.
## The idea
A number `n` is prime if no number from 2 to sqrt(n) divides it. Two steps
shrink the work:
1. **Skip most candidates.** Every prime above 3 has the form 6k - 1 or 6k + 1
(5, 7, 11, 13, 17, 19, 23, 25, ...). Even numbers and multiples of 3 are
rejected before the loop, so only these divisors are tried.
2. **Test four at a time.** One 256-bit AVX2 register holds four `double`
values, so one divide, one multiply and one compare handle four divisors.
Each loop pass tests the divisors `d, d+2, d+6, d+8` and then moves 12 forward:
`5,7,11,13` then `17,19,23,25` then `29,31,35,37`, and so on.
## The code, step by step
```c
// Only divisors up to sqrt(n) need testing: a factor above it pairs with one below it.
const double limit = floor(sqrt((double)n));
// Broadcast n and the limit into all 4 double lanes of a 256-bit register.
__m256d v_n = _mm256_set1_pd((double)n);
__m256d v_limit = _mm256_set1_pd(limit);
// Divisors 5, 7, 11, 13 (6k +/- 1); each iteration advances by 12.
// Kept as 4 x int32 in a 128-bit register, converted to double each pass.
__m128i v_div_i = _mm_setr_epi32(5, 7, 11, 13);
const __m128i v_step = _mm_set1_epi32(12);
// base is the first divisor of the group; stop when it passes sqrt(n).
for (double base = 5.0; base <= limit; base += 12.0) {
// Convert the 4 int32 divisors to 4 doubles.
__m256d v_d = _mm256_cvtepi32_pd(v_div_i);
// q = floor(n / d) for all 4 divisors at once.
__m256d v_q = _mm256_floor_pd(_mm256_div_pd(v_n, v_d));
// rem = n - q * d, which is n mod d.
__m256d v_rem = _mm256_sub_pd(v_n, _mm256_mul_pd(v_q, v_d));
// Lane mask: all bits set where the remainder is 0 (d divides n).
__m256d is_zero = _mm256_cmp_pd(v_rem, _mm256_setzero_pd(), _CMP_EQ_OQ);
// Lane mask: all bits set where the divisor is <= sqrt(n).
__m256d in_range = _mm256_cmp_pd(v_d, v_limit, _CMP_LE_OQ);
// A lane only counts if it divides n and the divisor is <= sqrt(n).
// movemask packs the sign bit of each lane into 4 bits; non-zero = a hit.
if (_mm256_movemask_pd(_mm256_and_pd(is_zero, in_range)) != 0)
return false;
// Move to the next four divisors: 5,7,11,13 -> 17,19,23,25 -> ...
v_div_i = _mm_add_epi32(v_div_i, v_step);
}
// No divisor found up to sqrt(n): n is prime.
```
## Why the `in_range` mask is needed
The last group can contain divisors above sqrt(n). Without the mask such a
lane could report a "factor" that is not one. For `n = 25` the limit is 5, so
only lane 0 (divisor 5) counts and 25 is correctly rejected; for `n = 29`
nothing divides it and 29 is prime.
## Why `double` and not `float` or integers
AVX2 has no integer divide, so the division is done in floating point. A
`double` holds every 32-bit integer exactly and IEEE division is correctly
rounded, so `floor(n / d)` is exact for `n` up to `UINT32_MAX`. A 32-bit
`float` has only 24 bits of precision and would give wrong quotients. This is
also why the function supports `n` only up to 4,294,967,295.
## Intrinsics used
| Intrinsic | What it does |
| --- | --- |
| `_mm256_set1_pd` | copy one double into all 4 lanes |
| `_mm_setr_epi32` | build 4 int32 values in order |
| `_mm256_cvtepi32_pd` | convert 4 int32 to 4 double |
| `_mm256_div_pd` / `_mm256_mul_pd` / `_mm256_sub_pd` | divide, multiply, subtract in 4 lanes |
| `_mm256_floor_pd` | round 4 doubles down |
| `_mm256_cmp_pd` | compare 4 lanes, giving an all-ones or all-zeros mask per lane |
| `_mm256_and_pd` | bitwise AND of two masks |
| `_mm256_movemask_pd` | collapse the 4 lane masks into a 4-bit integer |
| `_mm_add_epi32` | add 4 int32 values |
They come from `<immintrin.h>`. Build with `-mavx2` (gcc/clang) or
`/arch:AVX2` (MSVC); on a CPU without AVX2 the program would crash with an
illegal instruction.
+14
View File
@@ -0,0 +1,14 @@
# Resultater
🌐 [English](Results) · **Dansk** · [Bahasa Melayu](Results-ms)
Tid til at finde alle primtal fra 0 til 1.000.000. Kun søgningen måles.
| CPU | Resultat |
| --- | --- |
| 13th Gen Intel(R) Core(TM) i5-13600K | Found 78498 primes from 0 to 1000000 in 20.977 ms |
Et resultat kan kun sammenlignes med et andet, når koden, compileren og flagene
er de samme. Sådan tilføjer du et resultat: kør programmet på din maskine (se
[Byg og kør](Build-and-Run-da)) og tilføj en række med dit CPU-navn og
opsummeringslinjen.
+15
View File
@@ -0,0 +1,15 @@
# Keputusan
🌐 [English](Results) · [Dansk](Results-da) · **Bahasa Melayu**
Masa untuk mencari semua nombor perdana dari 0 hingga 1,000,000. Hanya carian
diukur.
| CPU | Keputusan |
| --- | --- |
| 13th Gen Intel(R) Core(TM) i5-13600K | Found 78498 primes from 0 to 1000000 in 20.977 ms |
Keputusan hanya boleh dibandingkan dengan yang lain apabila kod, pengkompil dan
bendera sama. Untuk menambah keputusan, jalankan program pada mesin anda (lihat
[Bina dan jalankan](Build-and-Run-ms)) dan tambah satu baris dengan nama CPU
anda dan baris ringkasan.
+1 -1
View File
@@ -1,6 +1,6 @@
# Results
**English**
🌐 **English** · [Dansk](Results-da) · [Bahasa Melayu](Results-ms)
Time to find all primes from 0 to 1,000,000. Only the search is timed.
+9
View File
@@ -1,4 +1,13 @@
**prime-numbers-c**
- [Home](Home)
- [Description](Description)
- [How AVX2 works](How-AVX2-Works)
- [Results](Results)
- [Build and run](Build-and-Run)
**Languages**
- English: [Home](Home)
- Dansk: [Hjem](Home-da)
- Bahasa Melayu: [Utama](Home-ms)
+13 -1
View File
@@ -18,30 +18,42 @@ bool is_prime_avx2(uint64_t n) {
if (n % 2 == 0 || n % 3 == 0) return false;
if (n > UINT32_MAX) return false; // out of supported range
// Only divisors up to sqrt(n) need testing: a factor above it pairs with one below it.
const double limit = floor(sqrt((double)n));
// Broadcast n and the limit into all 4 double lanes of a 256-bit register.
__m256d v_n = _mm256_set1_pd((double)n);
__m256d v_limit = _mm256_set1_pd(limit);
// Divisors 5, 7, 11, 13 (6k +/- 1); each iteration advances by 12.
// Kept as 4 x int32 in a 128-bit register, converted to double each pass.
__m128i v_div_i = _mm_setr_epi32(5, 7, 11, 13);
const __m128i v_step = _mm_set1_epi32(12);
// base is the first divisor of the group; stop when it passes sqrt(n).
for (double base = 5.0; base <= limit; base += 12.0) {
// Convert the 4 int32 divisors to 4 doubles.
__m256d v_d = _mm256_cvtepi32_pd(v_div_i);
// q = floor(n / d) for all 4 divisors at once.
__m256d v_q = _mm256_floor_pd(_mm256_div_pd(v_n, v_d));
// rem = n - q * d, which is n mod d.
__m256d v_rem = _mm256_sub_pd(v_n, _mm256_mul_pd(v_q, v_d));
// Lane mask: all bits set where the remainder is 0 (d divides n).
__m256d is_zero = _mm256_cmp_pd(v_rem, _mm256_setzero_pd(), _CMP_EQ_OQ);
// Lane mask: all bits set where the divisor is <= sqrt(n).
__m256d in_range = _mm256_cmp_pd(v_d, v_limit, _CMP_LE_OQ);
// A lane only counts if it divides n and the divisor is <= sqrt(n)
// A lane only counts if it divides n and the divisor is <= sqrt(n).
// movemask packs the sign bit of each lane into 4 bits; non-zero = a hit.
if (_mm256_movemask_pd(_mm256_and_pd(is_zero, in_range)) != 0)
return false;
// Move to the next four divisors: 5,7,11,13 -> 17,19,23,25 -> ...
v_div_i = _mm_add_epi32(v_div_i, v_step);
}
// No divisor found up to sqrt(n): n is prime.
return true;
}