# 만델브로 집합 계산 최적화: 스칼라 코드에서 SIMD, OpenMP, CUDA로
만델브로 집합은 복소평면의 각 점에 대해 독립적으로 계산됩니다: z_0 = 0에서 시작하여 z_{n+1} = z_n² + c 수열이 유계로 유지되는지 확인합니다. |z_n| > 2이면 수열이 발산합니다. 반복 횟수를 256으로 제한하여 계산이 종료되도록 합니다. 이 작업은 완벽하게 병렬화가 가능합니다—각 픽셀이 독립적으로 처리됩니다.
픽셀 색상은 |z|가 반경 2를 초과하는 반복 횟수에 따라 결정됩니다. 256단계 후에도 유계 내에 머무르는 점은 검은색으로 표시합니다.
스칼라 구현
기본 알고리즘은 C++에서 화면 좌표 [0, WIDTH] × [0, HEIGHT]를 복소평면 영역 [-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);
반복 루프는 z_x²와 z_y²를 별도로 저장하여 최적화되었습니다:
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++;
}
AMD Ryzen 5 5600H에서 1920×1080 해상도, 256 반복 시 -O2 옵션으로 컴파일된 스칼라 코드는 7.0 ± 0.1 FPS (GCC/Clang)를 달성합니다.
AVX2 벡터화
256비트 ymm 레지스터에서의 SIMD는 명령어당 8개의 float를 처리합니다. <x86intrin.h>의 인트린직은 벡터화에 대한 명시적 제어를 제공합니다.
주요 벡터:
_c_x: 기본 좌표 + 오프셋 [0..7] × c_step_x_z_x,_z_y,_z_x2,_z_y2,_z_xy: 8개 픽셀 상태_iterations: 반복 카운터 (int32)
오프셋 초기화:
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));
반복 루프에서:
- 반경 계산:
_radius2 = _mm256_add_ps(_z_x2, _z_y2) - 지속 마스크:
_mm256_cmp_ps(_radius2, _max_radius2, _CMP_LT_OS)→ 모두 -1.0 또는 0.0 - 활성 마스크 하에서 상태 업데이트
- 8개 픽셀 처리 후 X 방향으로 이동:
_c_x = _mm256_add_ps(_c_x, _8_c_steps_x)
마스크를 통해 8개 픽셀이 모두 탈출하면 동시에 루프를 종료할 수 있습니다.
OpenMP 멀티스레딩
OpenMP는 #pragma omp parallel for 지시어를 사용하여 행을 스레드에 분배합니다. 각 스레드는 자신의 픽셀 스트립을 독립적으로 계산합니다.
#pragma omp parallel for schedule(dynamic)
schedule(dynamic)은 부하 균형을 위해 사용되며—프랙탈 영역에 따라 반복 횟수가 다릅니다.
AVX2 + OpenMP를 6코어 Ryzen 5 5600H에서 결합하면 최대 40-50 FPS를 달성합니다.
CUDA를 사용한 GPU 가속
CUDA로 포팅하는 것은 간단합니다: 각 스레드가 하나의 픽셀을 계산합니다. 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;
}
실행: dim3 block(16,16); dim3 grid((width+15)/16, (height+15)/16); mandelbrot_cuda<<<grid,block>>>(...)
노트북 GPU (RTX 3050)에서 Full HD 해상도로 500+ FPS를 달성합니다.
| 구현 | FPS (Ryzen 5 5600H) |
|-----------------|----------------------|
| Scalar | 7.0 |
| AVX2 | 45.2 |
| AVX2+OpenMP | 48.1 |
| CUDA | 520+ |
주요 교훈
- 만델브로 집합은 SIMD, OpenMP, CUDA를 테스트하기 위한 병렬화가 매우 쉬운 벤치마크 작업입니다
- AVX2는 루프당 8개 픽셀을 처리하여 스칼라 코드 대비 ~6.5배 속도 향상을 제공합니다
- OpenMP는 6코어에서 동기화 오버헤드로 인해 6-7% 추가 향상을 줍니다
- CUDA는 수천 개의 병렬 스레드로 인해 orders-of-magnitude 속도 향상을 제공합니다
- 성능 측정은 렌더링을 제외합니다 (hyperfine, N회 실행 ~5s)
— Editorial Team
아직 댓글이 없습니다.