Optimisation du calcul de l'ensemble de Mandelbrot : du code scalaire à SIMD, OpenMP et CUDA
L'ensemble de Mandelbrot est calculé indépendamment pour chaque point du plan complexe : nous vérifions si la séquence z_{n+1} = z_n² + c reste bornée, en partant de z_0 = 0. Si |z_n| > 2, la séquence diverge. Limiter les itérations à 256 garantit la fin du calcul. La tâche est parfaitement parallélisable — chaque pixel est traité indépendamment.
La couleur du pixel dépend du nombre d'itérations au bout duquel |z| dépasse le rayon 2. Les points qui restent dans les bornes après 256 étapes sont colorés en noir.
Implémentation scalaire
L'algorithme de base en C++ mappe les coordonnées d'écran [0, WIDTH] × [0, HEIGHT] sur la région du plan complexe [-1, 1] × [-1, 1] :
float c_y = -1.0f + screen_y * (2.0f / WINDOW_HEIGHT);
float c_x = -1.0f + screen_x * (2.0f / WINDOW_WIDTH);
La boucle d'itération est optimisée en stockant z_x² et z_y² séparément :
while (z_x2 + z_y2 < MAX_RADIUS_2 && iterations < MAX_ITERATION_DEPTH) {
z_y = 2 * z_x * z_y + c_y;
z_x = z_x2 - z_y2 + c_x;
z_x2 = z_x * z_x;
z_y2 = z_y * z_y;
iterations++;
}
Sur un AMD Ryzen 5 5600H à 1920×1080 avec 256 itérations, le code scalaire avec -O2 délivre 7,0 ± 0,1 FPS (GCC/Clang).
Vectorisation AVX2
SIMD sur les registres ymm de 256 bits traite 8 flottants par instruction. Les intrinsèques de <x86intrin.h> offrent un contrôle explicite sur la vectorisation.
Vecteurs clés :
_c_x: coordonnée de base + offsets [0..7] × c_step_x_z_x,_z_y,_z_x2,_z_y2,_z_xy: états pour 8 pixels_iterations: compteurs d'itérations (int32)
Initialisation des offsets :
const __m256 _01234567 = _mm256_set_ps(7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f, 0.0f);
_c_x = _mm256_add_ps(_c_x, _mm256_mul_ps(_c_step_x, _01234567));
Dans la boucle d'itération :
- Calculer le rayon :
_radius2 = _mm256_add_ps(_z_x2, _z_y2) - Masque de continuation :
_mm256_cmp_ps(_radius2, _max_radius2, _CMP_LT_OS)→ tout -1,0 ou 0,0 - Mettre à jour les états sous le masque actif
- Décaler le long de X après traitement de 8 pixels :
_c_x = _mm256_add_ps(_c_x, _8_c_steps_x)
Le masque permet d'interrompre la boucle pour les 8 pixels simultanément si tous se sont échappés.
Multithreading OpenMP
OpenMP distribue les lignes entre les threads grâce à la directive #pragma omp parallel for. Chaque thread calcule indépendamment sa bande de pixels.
#pragma omp parallel for schedule(dynamic)
schedule(dynamic) pour l'équilibrage de charge — différentes régions fractales nécessitent un nombre variable d'itérations.
Combiner AVX2 + OpenMP sur le Ryzen 5 5600H à 6 cœurs donne jusqu'à 40-50 FPS.
Accélération GPU avec CUDA
Porter vers CUDA est simple : chaque thread calcule un pixel. Noyau CUDA :
__global__ void mandelbrot_cuda(float* output, int width, int height) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x >= width || y >= height) return;
float c_x = -2.0f + (float)x * scale_x;
float c_y = -1.5f + (float)y * scale_y;
float z_x = 0.0f, z_y = 0.0f;
int iter = 0;
while (z_x*z_x + z_y*z_y < 4.0f && iter < 256) {
float temp = z_x*z_x - z_y*z_y + c_x;
z_y = 2.0f * z_x * z_y + c_y;
z_x = temp;
iter++;
}
output[y*width + x] = (float)iter;
}
Lancement : dim3 block(16,16); dim3 grid((width+15)/16, (height+15)/16); mandelbrot_cuda<<<grid,block>>>(...)
Sur un GPU d'ordinateur portable (RTX 3050), il atteint 500+ FPS en Full HD.
| Implémentation | FPS (Ryzen 5 5600H) |
|---------------|----------------------|
| Scalaire | 7,0 |
| AVX2 | 45,2 |
| AVX2+OpenMP | 48,1 |
| CUDA | 520+ |
Points clés
- L'ensemble de Mandelbrot est un benchmark parfaitement parallèle pour tester SIMD, OpenMP et CUDA
- AVX2 fournit ~6,5x d'accélération par rapport au code scalaire en traitant 8 pixels par boucle
- OpenMP ajoute 6-7 % de gain sur 6 cœurs du fait du surcoût de synchronisation
- CUDA offre une accélération d'un ordre de grandeur grâce à des milliers de threads parallèles
- Les mesures de performance excluent le rendu (hyperfine, N runs ~5s)
— Editorial Team
Aucun commentaire pour le moment.