콘텐츠로 이동

STL Parallel Algorithms & Execution Policy

C++17은 STL 알고리즘에 실행 정책(Execution Policy)을 추가하여, 기존 순차 알고리즘을 코드 변경 최소화로 병렬화할 수 있게 했습니다. std::sort, std::transform, std::reduce 등 수십 개의 알고리즘이 병렬 실행을 지원합니다.


#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++20
std::sort(std::execution::unseq, data.begin(), data.end());
정책멀티스레드SIMD설명
seq아니오아니오기존 순차 실행
par아니오멀티스레드 병렬
par_unseq병렬 + 벡터화
unseq아니오단일 스레드 벡터화

#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;
}
#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;
}
#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;
}
#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;
}

#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=320ms

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_sort
  • std::find, std::find_if, std::count, std::count_if
  • std::copy, std::copy_if, std::move
  • std::transform, std::for_each
  • std::reduce, std::transform_reduce
  • std::inclusive_scan, std::exclusive_scan
  • std::fill, std::generate
  • std::replace, std::replace_if
  • std::remove, std::remove_if
  • std::reverse, std::rotate
  • std::unique, std::merge

6. 컴파일러 및 라이브러리 지원

섹션 제목: “6. 컴파일러 및 라이브러리 지원”
Terminal window
# 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.cpp

STL 병렬 알고리즘은 기존 코드에서 실행 정책 인자 하나만 추가하면 멀티코어를 활용할 수 있는 우아한 방법입니다. 단, 람다 내부에서 공유 상태 접근에는 thread-safe한 방법을 사용해야 하며, par_unseq에서는 뮤텍스를 완전히 배제해야 합니다. 데이터 크기가 수만 건 이상일 때 성능 이득이 뚜렷하게 나타납니다.