OpenMP

오류 제보
소프트웨어 수치해석 마지막 수정: 2026-11-01 04:41:27

○ 공개 검수 대기 AI 보조 작성 표시 기준 보기
상위 문서: 병렬 컴퓨팅

1. 개요

OpenMP
Open Multi-Processing
종류공유 메모리 병렬 API (지시문 + 런타임 + 환경변수)
언어C, C++, Fortran
실행 모델포크-조인
관리OpenMP ARB (1997~)
첫 판Fortran 1.0 (1997), C/C++ 1.0 (1998)
주요 판3.0 태스크(2008) · 4.0 가속기·SIMD(2013) · 5.0(2018) · 6.0(2024)
짝꿍노드 간은 MPI

#pragma omp parallel for 한 줄이면 빨라진다. 빨라지지 않는 이유를 찾는 데는 일주일이 걸린다.

OpenMP는 C·C++·Fortran 코드에 컴파일러 지시문을 얹어, 하나의 메모리를 공유하는 여러 스레드로 작업을 나누게 하는 표준 병렬 프로그래밍 인터페이스다. 지시문(#pragma omp …, Fortran 은 !$omp …), 런타임 함수(omp_get_thread_num() 등), 환경변수(OMP_NUM_THREADS 등) 세 가지로 이뤄진다. 컴파일 옵션(-fopenmp 등)을 끄면 지시문이 주석처럼 무시되어 같은 코드가 순차 프로그램으로 돌아간다 — 기존 코드를 조금씩 병렬화할 수 있다는 것이 처음부터의 설계 목표였다.

하드웨어·컴파일러 업체들이 모인 OpenMP ARB 가 1997년 Fortran 판, 1998년 C/C++ 판을 냈다. 그 전에는 업체마다 지시문이 달라 공유 메모리 코드를 옮길 때마다 새로 써야 했다.1 노드 하나 안의 코어를 쓰는 데는 OpenMP, 노드 사이는 MPI 라는 분업이 과학계산의 표준 구도이고, 그 큰 그림은 병렬 컴퓨팅 문서에 있다. 이 문서는 실제로 코드를 짤 때 마주치는 것 — 데이터 공유 규칙, 경쟁 상태, 스케줄링, 그리고 성능이 안 나오는 대표적인 이유들 — 을 다룬다.

2. 포크-조인과 데이터 공유

프로그램은 스레드 하나(마스터)로 시작한다. parallel 영역을 만나면 스레드 팀이 생기고(포크), 영역 끝에서 암묵적 장벽으로 모두 기다린 뒤 다시 하나가 된다(조인).

double sum = 0.0;
#pragma omp parallel for reduction(+:sum) schedule(static)
for (long i = 0; i < n; i++) {
    double xi = (i + 0.5) * h;     // 루프 안 선언 → 스레드마다 private
    sum += 4.0 / (1.0 + xi * xi);  // 각자 부분합, 끝에서 합침
}

핵심은 변수마다 공유(shared)냐 사유(private)냐다. 기본값은 영역 밖에서 선언된 변수는 공유, 루프 인덱스와 영역 안 선언은 사유다. 여기서 대부분의 버그가 나온다.

  • 경쟁 상태. 위에서 reduction 을 빼면 모든 스레드가 공유 변수 sum 을 동시에 읽고 쓴다. 결과가 실행마다 달라지고, 작은 테스트에서는 우연히 맞기도 한다. 해결책은 reduction, atomic, critical 순으로 비싸진다. critical 을 루프 본문에 넣으면 사실상 순차 실행이다.
  • default(none) 을 붙이면 모든 변수의 공유 속성을 명시하도록 컴파일러가 강제한다. 귀찮지만 경쟁 상태 디버깅 시간을 크게 줄이는 국룰이다.
  • firstprivate·lastprivate 는 사유 복사본을 영역 밖 값으로 초기화하거나, 마지막 반복의 값을 밖으로 돌려준다.
  • 환원의 비결정성. 부동소수점 덧셈은 결합법칙이 성립하지 않으므로 스레드 수나 스케줄이 바뀌면 reduction 결과가 마지막 몇 자리에서 달라진다. 비트 단위 재현성이 필요하면 고정 순서 트리 합이나 혼합 정밀도 기법(보상 합산)을 직접 짠다.

3. 일감 나누기 — 스케줄과 태스크

for 지시문의 schedule 절이 반복을 스레드에 나누는 방식을 정한다.

스케줄방식언제
static연속 덩어리로 미리 균등 분배반복마다 일이 비슷할 때. 오버헤드 최소, NUMA 친화
dynamic, c크기 c 덩어리를 끝난 스레드가 가져감반복마다 일이 크게 다를 때(적응 격자, 입자 수가 다른 셀)
guided덩어리가 점점 작아짐dynamic 의 오버헤드를 줄이고 싶을 때

중첩 루프는 collapse(2) 로 합쳐 반복 공간을 늘린다. 바깥 루프 길이가 스레드 수보다 작을 때 특히 필요하다.

규칙적인 루프로 표현이 안 되는 일 — 재귀적 분할, 트리 순회, 불규칙 그래프 — 은 3.0 판(2008)에서 들어온 태스크로 표현한다. task 로 작업 단위를 만들고 taskwait·depend 로 순서를 건다. 희소 행렬의 중첩절단 분해나 블록 촐레스키처럼 의존성 그래프가 있는 알고리즘이 태스크 의존성으로 깔끔하게 표현된다.

4.0 판(2013)부터는 두 축이 더 붙었다. simd 지시문은 루프를 SIMD 명령으로 벡터화하라고 컴파일러에 알려 주고, target 지시문은 계산을 GPU 같은 가속기로 내려보낸다(target teams distribute parallel for). 같은 소스로 CPU·GPU 를 모두 노린다는 점에서 GPU 컴퓨팅의 CUDA 직접 작성과 경쟁하지만, 성능을 끝까지 짜내려면 결국 데이터 이동(map 절)을 손으로 관리해야 한다.

4. 왜 안 빨라지나

OpenMP 는 “병렬화했다”와 “빨라졌다” 사이의 거리가 큰 도구다. 반복되는 원인 넷.

1. 메모리 대역폭 포화. 스텐실 연산이나 벡터 갱신 같은 메모리 바운드 커널은 코어를 늘려도 소켓의 메모리 대역폭 이상으로 빨라지지 않는다. 보통 소켓당 코어의 일부만 써도 대역폭이 포화되어 확장 곡선이 꺾인다. 루프라인 모델로 먼저 천장을 계산하고, 스레드 수를 늘리며 그 천장에 닿는지 보는 것이 순서다.

2. 퍼스트 터치와 NUMA. 다중 소켓 노드에서 메모리 페이지는 처음 쓴 스레드가 있는 소켓에 배치된다(리눅스 기본 정책). 배열을 마스터 스레드 하나가 0 으로 초기화하면 전부 소켓 0 에 몰리고, 다른 소켓의 스레드는 원격 메모리를 읽느라 느려진다. 해결은 초기화 루프도 계산 루프와 같은 static 스케줄로 병렬화하는 것, 그리고 OMP_PROC_BIND=close·OMP_PLACES=cores 로 스레드를 코어에 고정해 운영체제가 스레드를 옮기지 못하게 하는 것이다.

3. 거짓 공유(false sharing). 스레드들이 서로 다른 변수를 쓰더라도 그 변수들이 같은 캐시 라인(대개 64 바이트)에 있으면, 한 스레드가 쓸 때마다 다른 코어의 그 라인이 무효화된다. double partial[nthreads] 배열에 스레드별 부분합을 누적하는 코드가 전형적인 희생자다. reduction 을 쓰거나, 스레드별 데이터를 캐시 라인 크기로 띄운다.

4. 순차 구간과 동기화. 병렬 영역 밖에 남은 코드, 매 반복의 암묵적 장벽, 작은 루프마다 반복되는 포크-조인 비용이 쌓인다. 암달의 법칙(병렬 컴퓨팅 참고)대로 순차 비율 5% 면 코어가 무한대여도 20 배가 상한이다. 큰 parallel 영역 하나 안에 여러 for 를 넣고, 필요 없는 장벽은 nowait 로 없앤다.2

5. MPI 와 함께 — 하이브리드

수천 코어 이상에서는 노드 사이를 MPI, 노드 안을 OpenMP 로 나누는 하이브리드 구성이 흔하다. 순수 MPI 대비 장점은 노드당 MPI 랭크 수가 줄어 영역 분할법의 경계(고스트 셀) 메모리와 메시지 수가 줄어드는 것, 노드 안에서 메모리를 공유해 큰 테이블을 한 벌만 두는 것이다. 단점은 두 모델의 함정을 다 갖는다는 것이다. MPI 를 MPI_Init_thread 로 초기화하고, 어느 스레드가 MPI 를 부를지(FUNNELED·MULTIPLE 등)를 명시해야 한다. 소켓마다 MPI 랭크 하나, 그 안을 OpenMP 로 채우는 배치가 NUMA 문제를 피하는 무난한 출발점이다.

실무 체크리스트:

  • 스레드 1 개 성능부터 최적화한다. 느린 순차 코드를 병렬화하면 느린 병렬 코드가 된다.
  • OMP_NUM_THREADS·OMP_PROC_BIND·OMP_PLACES 를 실행 스크립트에 명시한다. 기본값은 구현마다 다르다.
  • 강 스케일링 곡선을 1, 2, 4, … 코어로 찍어 꺾이는 지점을 확인한다. 꺾이는 곳이 대역폭 포화인지 순차 구간인지가 다음 할 일을 정한다.
  • 경쟁 상태는 ThreadSanitizer 같은 도구로 잡는다. 결과가 “대체로 맞는” 병렬 코드는 틀린 코드다.3

6. 관련 문서

7. Footnotes

  1. 1990년대 중반 공유 메모리 기계마다(SGI, Cray, Convex…) 병렬 지시문 방언이 따로 있었다. 1980년대 말 표준화 시도(ANSI X3H5)가 흐지부지된 뒤 업체들이 다시 모여 만든 것이 OpenMP 다. 표준이 늦게 나오면 방언이 먼저 굳는다는 교훈. ↩

  2. “병렬 영역 안에 for 가 열 개 있는데 장벽이 열 개”인 코드는 흔하다. 장벽 하나는 마이크로초 단위지만, 시간 스텝마다 수천 번 돌면 그게 계산보다 길어진다. 프로파일러를 켜 보면 스레드들이 대부분 서로를 기다리고 있다. ↩

  3. 경쟁 상태는 디버그 빌드에서 사라지고 최적화 빌드에서만 나타나는 경우가 많아 “하이젠버그”라 불린다. 출력문을 넣으면 타이밍이 바뀌어 버그가 숨는다. 관측하면 사라지는 것까지 양자역학을 닮았다. ↩

검증 자료

이 문서의 주요 출처

  1. OpenMP Architecture Review Board, OpenMP API Specifications (최신판 및 이전 판 목록)
  2. L. Dagum, R. Menon (1998), OpenMP: an industry standard API for shared-memory programming, IEEE Computational Science and Engineering 5(1), 46-55
  3. B. Chapman, G. Jost, R. van der Pas (2007), Using OpenMP: Portable Shared Memory Parallel Programming, MIT Press
  4. G. Hager, G. Wellein (2010), Introduction to High Performance Computing for Scientists and Engineers, CRC Press (Ch. 6-8, OpenMP and ccNUMA)
이 문서가 문제를 푸는 데 도움이 됐나요? 한 번의 클릭이 다음 검수 순서를 정합니다.

의견을 반영할게요. 감사합니다.

다음으로 읽기 병렬 컴퓨팅 약 5분 더 탐색 →