STL Parallel Algorithms & Execution Policy
C++17은 STL 알고리즘에 실행 정책(Execution Policy)을 추가하여, 기존 순차 알고리즘을 코드 변경 최소화로 병렬화할 수 있게 했습니다. std::sort, std::transform, std::reduce 등 수십 개의 알고리즘이 병렬 실행을 지원합니다.
1. 실행 정책 종류
섹션 제목: “1. 실행 정책 종류”#include <execution>#include <algorithm>#include <vector>
std::vector<int> data(1'000'000);
// 순차 실행 (기존과 동일)std::sort(std::execution::seq, data.begin(), data.end());
// 병렬 실행 (멀티스레드)std::sort(std::execution::par, data.begin(), data.end());
// 병렬 + 벡터화 (멀티스레드 + SIMD)std::sort(std::execution::par_unseq, data.begin(), data.end());
// 벡터화만 (단일 스레드 + SIMD) - C++20std::sort(std::execution::unseq, data.begin(), data.end());| 정책 | 멀티스레드 | SIMD | 설명 |
|---|---|---|---|
seq | 아니오 | 아니오 | 기존 순차 실행 |
par | 예 | 아니오 | 멀티스레드 병렬 |
par_unseq | 예 | 예 | 병렬 + 벡터화 |
unseq | 아니오 | 예 | 단일 스레드 벡터화 |
2. 주요 알고리즘 실전 예제
섹션 제목: “2. 주요 알고리즘 실전 예제”2.1 std::transform (병렬 변환)
섹션 제목: “2.1 std::transform (병렬 변환)”#include <execution>#include <algorithm>#include <vector>#include <cmath>
int main() { const std::size_t N = 10'000'000; std::vector<double> input(N), output(N);
// 입력 초기화 std::iota(input.begin(), input.end(), 0.0);
// 병렬 변환: 각 원소에 sqrt 적용 std::transform( std::execution::par_unseq, input.begin(), input.end(), output.begin(), [](double x) { return std::sqrt(x); } );
return 0;}2.2 std::reduce (병렬 합산)
섹션 제목: “2.2 std::reduce (병렬 합산)”#include <execution>#include <numeric>#include <vector>
int main() { std::vector<long long> data(10'000'000); std::iota(data.begin(), data.end(), 1LL);
// std::accumulate는 순차만 지원 // std::reduce는 병렬 + 교환법칙 성립하는 연산 지원 long long sum = std::reduce( std::execution::par, data.begin(), data.end(), 0LL, std::plus<long long>{} );
// 주의: reduce는 연산 순서 보장 안 됨 (부동소수점에서 결과 차이 가능) return 0;}2.3 std::transform_reduce (맵-리듀스)
섹션 제목: “2.3 std::transform_reduce (맵-리듀스)”#include <execution>#include <numeric>#include <vector>
// 내적 (dot product) 병렬 계산double dot_product(const std::vector<double>& a, const std::vector<double>& b) { return std::transform_reduce( std::execution::par_unseq, a.begin(), a.end(), b.begin(), 0.0, // 초기값 std::plus<double>{}, // 리듀스 연산 std::multiplies<double>{} // 변환 연산 );}
int main() { std::vector<double> a = {1.0, 2.0, 3.0, 4.0}; std::vector<double> b = {5.0, 6.0, 7.0, 8.0}; double result = dot_product(a, b); // 1*5 + 2*6 + 3*7 + 4*8 = 70 return 0;}2.4 std::for_each (병렬 순회)
섹션 제목: “2.4 std::for_each (병렬 순회)”#include <execution>#include <algorithm>#include <atomic>#include <vector>
int main() { std::vector<int> data(1'000'000, 1); std::atomic<long long> sum{0};
std::for_each( std::execution::par, data.begin(), data.end(), [&sum](int val) { sum.fetch_add(val, std::memory_order_relaxed); } ); // sum == 1,000,000 return 0;}3. 병렬 정렬 성능 측정
섹션 제목: “3. 병렬 정렬 성능 측정”#include <execution>#include <algorithm>#include <vector>#include <random>#include <chrono>#include <iostream>
template<typename Policy, typename Container>auto measure(Policy policy, Container data) { auto start = std::chrono::high_resolution_clock::now(); std::sort(policy, data.begin(), data.end()); auto end = std::chrono::high_resolution_clock::now(); return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();}
int main() { const std::size_t N = 50'000'000; std::vector<int> data(N); std::mt19937 rng(42); std::uniform_int_distribution<int> dist(0, 1'000'000); std::generate(data.begin(), data.end(), [&]{ return dist(rng); });
auto t_seq = measure(std::execution::seq, data); auto t_par = measure(std::execution::par, data); auto t_par_unseq = measure(std::execution::par_unseq, data);
std::cout << "seq: " << t_seq << " ms\n"; std::cout << "par: " << t_par << " ms\n"; std::cout << "par_unseq: " << t_par_unseq << " ms\n"; std::cout << "speedup (par): " << (double)t_seq / t_par << "x\n";
return 0;}// 8코어 기준 예시: seq=2500ms, par=380ms, par_unseq=320ms4. par_unseq 사용 시 주의사항
섹션 제목: “4. par_unseq 사용 시 주의사항”par_unseq는 SIMD 벡터화와 멀티스레드를 동시에 활용합니다. 람다 내부에서 다음을 피해야 합니다.
// 잘못된 예 - par_unseq에서 mutex 사용 불가 (벡터화 불가, 데드락 위험)std::mutex mtx;std::for_each(std::execution::par_unseq, v.begin(), v.end(), [&](int x) { std::lock_guard lock(mtx); // 절대 금지 shared_value += x; });
// 올바른 예 - atomic 사용std::atomic<int> total{0};std::for_each(std::execution::par_unseq, v.begin(), v.end(), [&](int x) { total.fetch_add(x, std::memory_order_relaxed); });5. 사용 가능한 병렬 알고리즘 목록
섹션 제목: “5. 사용 가능한 병렬 알고리즘 목록”std::sort,std::stable_sort,std::partial_sortstd::find,std::find_if,std::count,std::count_ifstd::copy,std::copy_if,std::movestd::transform,std::for_eachstd::reduce,std::transform_reducestd::inclusive_scan,std::exclusive_scanstd::fill,std::generatestd::replace,std::replace_ifstd::remove,std::remove_ifstd::reverse,std::rotatestd::unique,std::merge
6. 컴파일러 및 라이브러리 지원
섹션 제목: “6. 컴파일러 및 라이브러리 지원”# GCC: Intel TBB 또는 OpenMP 백엔드 필요g++ -std=c++17 -O2 -ltbb main.cpp
# MSVC: 내장 병렬 백엔드 (추가 라이브러리 불필요)cl /std:c++17 /O2 main.cpp
# Clang: LLVM PSTL 또는 TBB 필요clang++ -std=c++17 -O2 -ltbb main.cppSTL 병렬 알고리즘은 기존 코드에서 실행 정책 인자 하나만 추가하면 멀티코어를 활용할 수 있는 우아한 방법입니다. 단, 람다 내부에서 공유 상태 접근에는 thread-safe한 방법을 사용해야 하며, par_unseq에서는 뮤텍스를 완전히 배제해야 합니다. 데이터 크기가 수만 건 이상일 때 성능 이득이 뚜렷하게 나타납니다.