Retour à l'accueil

Optimisation SIMD de Mandelbrot AVX2 OpenMP CUDA

L'article décompose étape par étape l'optimisation du calcul de l'ensemble de Mandelbrot : du C++ scalaire (7 FPS) à AVX2 SIMD (45 FPS), OpenMP (48 FPS) et CUDA (520+ FPS). Codes intrinsèques complets, multithreading et implémentation GPU avec benchmarks sur AMD Ryzen 5 5600H et RTX 3050.

De 7 à 520 FPS : vectorisation de Mandelbrot SIMD+CUDA
Advertisement 728x90

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] :

Google AdInline article slot
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.

Google AdInline article slot

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 :

Google AdInline article slot
  • 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

Advertisement 728x90

Lire ensuite