C++ Parallel Algorithms | "병렬 알고리즘" 가이드
이 글의 핵심
C++ Parallel Algorithms에 대한 실전 가이드입니다.
들어가며
C++17 병렬 알고리즘은 멀티코어 CPU를 활용하여 표준 알고리즘을 자동으로 병렬화합니다. std::execution::par를 추가하는 것만으로 std::sort, std::transform 등을 병렬 실행할 수 있습니다.
1. Execution Policy
실행 정책 종류
#include <algorithm>
#include <execution>
#include <vector>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// 1. seq: 순차 실행 (기본)
std::sort(std::execution::seq, v.begin(), v.end());
// 2. par: 병렬 실행
std::sort(std::execution::par, v.begin(), v.end());
// 3. par_unseq: 병렬 + 벡터화
std::sort(std::execution::par_unseq, v.begin(), v.end());
return 0;
}
Execution Policy 비교
| 정책 | 설명 | 특징 | 사용 시기 |
|---|---|---|---|
| seq | 순차 실행 | 단일 스레드 | 작은 데이터 |
| par | 병렬 실행 | 멀티스레드 | 큰 데이터, 독립적 연산 |
| par_unseq | 병렬 + 벡터화 | SIMD + 멀티스레드 | 큰 데이터, 단순 연산 |
2. 병렬 정렬
기본 정렬
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
#include <chrono>
void benchmarkSort() {
std::vector<int> v1(10000000);
std::generate(v1.begin(), v1.end(), std::rand);
auto v2 = v1;
// 순차 정렬
auto start = std::chrono::high_resolution_clock::now();
std::sort(v1.begin(), v1.end());
auto end = std::chrono::high_resolution_clock::now();
auto seq_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// 병렬 정렬
start = std::chrono::high_resolution_clock::now();
std::sort(std::execution::par, v2.begin(), v2.end());
end = std::chrono::high_resolution_clock::now();
auto par_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "순차 정렬: " << seq_time.count() << "ms" << std::endl;
std::cout << "병렬 정렬: " << par_time.count() << "ms" << std::endl;
std::cout << "속도 향상: " << (double)seq_time.count() / par_time.count() << "x" << std::endl;
}
int main() {
benchmarkSort();
return 0;
}
출력 (4코어):
순차 정렬: 2341ms
병렬 정렬: 687ms
속도 향상: 3.4x
3. 병렬 변환
transform
#include <algorithm>
#include <execution>
#include <vector>
#include <cmath>
#include <iostream>
int main() {
std::vector<double> data(10000000);
std::iota(data.begin(), data.end(), 1.0);
// 병렬 제곱근 계산
std::transform(std::execution::par,
data.begin(), data.end(), data.begin(),
{ return std::sqrt(x); });
std::cout << "첫 5개: ";
for (int i = 0; i < 5; ++i) {
std::cout << data[i] << " ";
}
std::cout << std::endl;
return 0;
}
출력:
첫 5개: 1 1.41421 1.73205 2 2.23607
for_each
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(1000000);
std::iota(v.begin(), v.end(), 1);
// 병렬 for_each
std::for_each(std::execution::par, v.begin(), v.end(),
{
x = x * x; // 제곱
});
std::cout << "v[99]: " << v[99] << std::endl; // 10000
return 0;
}
4. 병렬 집계
reduce
#include <numeric>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10000000);
std::iota(v.begin(), v.end(), 1);
// 병렬 reduce (합계)
long long sum = std::reduce(std::execution::par,
v.begin(), v.end(), 0LL);
std::cout << "합: " << sum << std::endl;
return 0;
}
출력:
합: 50000005000000
transform_reduce
#include <numeric>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(1000000);
std::iota(v.begin(), v.end(), 1);
// 병렬 제곱합
long long sum_of_squares = std::transform_reduce(
std::execution::par,
v.begin(), v.end(),
0LL,
std::plus<>(),
{ return static_cast<long long>(x) * x; }
);
std::cout << "제곱합: " << sum_of_squares << std::endl;
return 0;
}
5. 병렬 검색
find
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10000000);
std::iota(v.begin(), v.end(), 1);
// 병렬 find
auto it = std::find(std::execution::par, v.begin(), v.end(), 5000000);
if (it != v.end()) {
std::cout << "찾음: " << *it << std::endl;
std::cout << "인덱스: " << std::distance(v.begin(), it) << std::endl;
}
return 0;
}
count_if
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10000000);
std::generate(v.begin(), v.end(), std::rand);
// 병렬 count_if (짝수 개수)
auto count = std::count_if(std::execution::par, v.begin(), v.end(),
{ return x % 2 == 0; });
std::cout << "짝수 개수: " << count << std::endl;
return 0;
}
6. 자주 발생하는 문제
문제 1: 작은 데이터
#include <algorithm>
#include <execution>
#include <vector>
#include <chrono>
#include <iostream>
void benchmarkSmallData() {
std::vector<int> small(100);
std::generate(small.begin(), small.end(), std::rand);
auto v1 = small;
auto v2 = small;
// 순차
auto start = std::chrono::high_resolution_clock::now();
std::sort(v1.begin(), v1.end());
auto end = std::chrono::high_resolution_clock::now();
auto seq_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// 병렬 (오버헤드 > 이득)
start = std::chrono::high_resolution_clock::now();
std::sort(std::execution::par, v2.begin(), v2.end());
end = std::chrono::high_resolution_clock::now();
auto par_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "순차: " << seq_time.count() << "μs" << std::endl;
std::cout << "병렬: " << par_time.count() << "μs" << std::endl;
}
int main() {
benchmarkSmallData();
// 순차: 5μs
// 병렬: 150μs (오버헤드)
return 0;
}
문제 2: 공유 상태 (데이터 레이스)
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(1000000);
std::iota(v.begin(), v.end(), 1);
int sum = 0;
// ❌ 데이터 레이스
std::for_each(std::execution::par, v.begin(), v.end(), [&](int x) {
sum += x; // 여러 스레드가 sum에 동시 접근
});
std::cout << "잘못된 합: " << sum << std::endl; // 예상과 다름
// ✅ reduce 사용
long long correct_sum = std::reduce(std::execution::par, v.begin(), v.end(), 0LL);
std::cout << "올바른 합: " << correct_sum << std::endl;
return 0;
}
문제 3: 예외 처리
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, -3, 4, -5};
try {
// 병렬 실행 중 예외
std::for_each(std::execution::par, v.begin(), v.end(), {
if (x < 0) {
throw std::runtime_error("음수 발견");
}
});
} catch (const std::exception& e) {
// 여러 스레드에서 예외 발생 가능
// 첫 번째 예외만 잡힘
std::cout << "예외: " << e.what() << std::endl;
}
return 0;
}
문제 4: 순서 의존 연산
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// ❌ 순서 의존 (레이스)
std::for_each(std::execution::par, v.begin(), v.end(), [&](int& x) {
x = v[0] + 1; // v[0]을 여러 스레드가 읽음 (위험)
});
// ✅ 독립적 연산
std::for_each(std::execution::par, v.begin(), v.end(), {
x = x * 2; // 각 요소 독립적
});
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
7. 지원 알고리즘
주요 알고리즘
#include <algorithm>
#include <numeric>
#include <execution>
// 정렬
std::sort(std::execution::par, v.begin(), v.end());
std::stable_sort(std::execution::par, v.begin(), v.end());
std::partial_sort(std::execution::par, v.begin(), v.begin() + 10, v.end());
// 검색
std::find(std::execution::par, v.begin(), v.end(), 42);
std::find_if(std::execution::par, v.begin(), v.end(), pred);
std::binary_search(std::execution::par, v.begin(), v.end(), 42);
// 변환
std::transform(std::execution::par, v.begin(), v.end(), v.begin(), func);
std::for_each(std::execution::par, v.begin(), v.end(), func);
// 집계
std::reduce(std::execution::par, v.begin(), v.end(), 0);
std::transform_reduce(std::execution::par, v.begin(), v.end(), 0, plus, func);
// 복사
std::copy(std::execution::par, v1.begin(), v1.end(), v2.begin());
std::copy_if(std::execution::par, v1.begin(), v1.end(), v2.begin(), pred);
// 기타
std::count(std::execution::par, v.begin(), v.end(), 42);
std::count_if(std::execution::par, v.begin(), v.end(), pred);
std::all_of(std::execution::par, v.begin(), v.end(), pred);
std::any_of(std::execution::par, v.begin(), v.end(), pred);
8. 실전 예제: 이미지 처리
#include <algorithm>
#include <execution>
#include <vector>
#include <cmath>
#include <iostream>
struct Pixel {
unsigned char r, g, b;
};
class ImageProcessor {
std::vector<Pixel> pixels;
int width, height;
public:
ImageProcessor(int w, int h) : width(w), height(h) {
pixels.resize(w * h);
}
// 병렬 그레이스케일 변환
void toGrayscale() {
std::for_each(std::execution::par, pixels.begin(), pixels.end(),
{
unsigned char gray = static_cast<unsigned char>(
0.299 * p.r + 0.587 * p.g + 0.114 * p.b
);
p.r = p.g = p.b = gray;
});
}
// 병렬 밝기 조정
void adjustBrightness(int delta) {
std::for_each(std::execution::par, pixels.begin(), pixels.end(),
[delta](Pixel& p) {
auto clamp = {
return std::max(0, std::min(255, val));
};
p.r = clamp(p.r + delta);
p.g = clamp(p.g + delta);
p.b = clamp(p.b + delta);
});
}
// 병렬 블러 (간단한 평균)
void blur() {
auto original = pixels;
std::for_each(std::execution::par, pixels.begin(), pixels.end(),
[&](Pixel& p) {
int idx = &p - &pixels[0];
int x = idx % width;
int y = idx / width;
// 3x3 평균 (간단화)
int r_sum = 0, g_sum = 0, b_sum = 0, count = 0;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
int nidx = ny * width + nx;
r_sum += original[nidx].r;
g_sum += original[nidx].g;
b_sum += original[nidx].b;
++count;
}
}
}
p.r = r_sum / count;
p.g = g_sum / count;
p.b = b_sum / count;
});
}
// 병렬 픽셀 카운트
int countBrightPixels(int threshold) {
return std::count_if(std::execution::par, pixels.begin(), pixels.end(),
[threshold](const Pixel& p) {
int brightness = (p.r + p.g + p.b) / 3;
return brightness > threshold;
});
}
};
int main() {
ImageProcessor img(1920, 1080);
img.toGrayscale();
img.adjustBrightness(20);
img.blur();
int bright = img.countBrightPixels(128);
std::cout << "밝은 픽셀: " << bright << std::endl;
return 0;
}
정리
핵심 요약
- 병렬 알고리즘: C++17 멀티코어 활용
- Execution Policy: seq, par, par_unseq
- 성능: 큰 데이터에서 2-4배 향상
- 주의: 데이터 레이스, 순서 의존 금지
- 적용: 10,000개 이상, 독립적 연산
병렬 알고리즘 효과
| 데이터 크기 | 연산 복잡도 | 병렬 효과 | 권장 정책 |
|---|---|---|---|
| < 1,000 | 낮음 | 없음 | seq |
| 1,000-10,000 | 낮음 | 낮음 | seq |
| > 10,000 | 낮음 | 중간 | par |
| > 10,000 | 높음 | 높음 | par |
| > 100,000 | 단순 | 매우 높음 | par_unseq |
실전 팁
사용 원칙:
- 큰 데이터 (10,000개 이상)에만 사용
- 계산 집약적 연산에 효과적
- 독립적 연산만 병렬화
- 공유 상태 피하기
성능:
- 프로파일링으로 효과 측정
- 작은 데이터는 오버헤드 큼
- 메모리 집약적 연산은 효과 낮음
- CPU 코어 수에 따라 효과 다름
주의사항:
- 데이터 레이스 방지 (
std::atomic,reduce) - 예외 처리 신중 (
std::terminate가능) - 순서 의존 연산 금지
- 디버깅 어려움 (TSan 사용)
다음 단계
- C++ Execution Policy
- C++ Thread
- C++ Atomic
관련 글
- C++ Algorithm Sort |
- 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리