본문으로 건너뛰기 C++ Parallel Algorithms | '병렬 알고리즘' 가이드

C++ Parallel Algorithms | '병렬 알고리즘' 가이드

C++ Parallel Algorithms | '병렬 알고리즘' 가이드

이 글의 핵심

C++ Parallel Algorithms: "병렬 알고리즘" 가이드. Execution Policy·병렬 정렬.

들어가며

C++17 병렬 알고리즘은 멀티코어 CPU를 활용하여 표준 알고리즘을 자동으로 병렬화합니다. std::execution::par를 추가하는 것만으로 std::sort, std::transform 등을 병렬 실행할 수 있습니다.


코딩 테스트 준비하며 깨달은 것

알고리즘 문제를 풀다 보면 “이게 실무에 무슨 도움이 될까?” 하는 의문이 들 때가 있습니다. 저도 그랬습니다. 하지만 실제 프로젝트에서 성능 문제에 부딪히면, 알고리즘 지식이 얼마나 중요한지 깨닫게 됩니다. 예를 들어, 사용자 검색 기능이 느려서 고민하다가 해시 테이블을 적용하니 응답 시간이 10초에서 0.1초로 줄어든 경험이 있습니다.

코딩 테스트는 단순히 취업을 위한 관문이 아니라, 문제 해결 능력을 키우는 훈련장입니다. 처음엔 브루트 포스로 풀다가, 시간 복잡도를 개선하는 과정에서 “아, 이렇게 생각하면 되는구나” 하는 깨달음을 얻을 때의 쾌감은 말로 표현하기 어렵습니다. 이 글에서는 단순히 정답 코드만 제시하는 게 아니라, 문제를 어떻게 접근하고 최적화하는지 사고 과정을 함께 공유하겠습니다.

1. Execution Policy

C++17 이전에는 표준 알고리즘을 병렬로 실행하려면 std::thread나 OpenMP 같은 외부 도구를 직접 다뤄야 했습니다. C++17부터는 <execution> 헤더의 실행 정책(execution policy)을 알고리즘의 첫 번째 인자로 넘기기만 하면, 표준 라이브러리 구현체가 알아서 스레드를 나누고 작업을 분배해줍니다. 즉 알고리즘 로직은 그대로 두고 정책 하나만 바꿔서 순차·병렬·벡터화 실행을 자유롭게 전환할 수 있다는 점이 이 기능의 가장 큰 매력입니다.

실행 정책 종류

std::execution에는 세 가지 기본 정책이 있습니다. seq는 기존과 동일한 순차 실행이고, par는 여러 스레드에 작업을 나누어 병렬로 실행하며, par_unseq는 병렬 실행에 더해 SIMD 명령어를 활용한 벡터화까지 허용합니다. 다만 par_unseq를 사용할 때는 실행 중인 함수 내부에서 메모리 할당이나 락(lock) 같은 벡터화와 충돌하는 연산을 하면 안 된다는 제약이 따르므로, 단순 산술 연산처럼 부작용이 없는 코드에만 적용하는 것이 안전합니다.

#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. 병렬 정렬

정렬은 병렬 알고리즘의 효과를 가장 체감하기 쉬운 연산입니다. 데이터를 여러 구간으로 나누어 각 구간을 독립적으로 정렬한 뒤 병합하는 방식으로 동작하기 때문에, 코어 수가 늘어날수록 처리 시간이 비례해서 줄어드는 경향을 보입니다.

기본 정렬

같은 1천만 개의 무작위 정수 벡터를 두 개 복사본으로 만들어, 하나는 std::sort(v1.begin(), v1.end())로 순차 정렬하고 다른 하나는 std::execution::par를 붙여 병렬 정렬한 뒤 소요 시간을 비교하는 벤치마크입니다. std::chrono::high_resolution_clock으로 각각의 실행 시간을 측정해 두 값의 비율을 출력하면, 병렬 실행이 실제로 얼마나 속도를 향상시키는지를 직관적으로 확인할 수 있습니다.

#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. 병렬 변환

transformfor_each는 컨테이너의 각 원소를 독립적으로 변형하는 대표적인 병렬화 대상입니다. 원소 간에 서로 의존성이 없는 순수한 element-wise 연산일수록 병렬화 효과가 크게 나타납니다.

transform

std::transformstd::execution::par를 넘기면 1천만 개 원소 각각에 대해 제곱근 계산을 여러 스레드에 나누어 수행합니다. 각 원소의 제곱근을 구하는 연산은 다른 원소의 값에 전혀 의존하지 않는 완전히 독립적인 계산이기 때문에, 스레드 간 동기화 없이도 안전하게 병렬 처리할 수 있는 전형적인 사례입니다.

#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(),
        [](double x) { 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

std::for_each는 반환값 없이 각 원소를 제자리에서 수정할 때 사용합니다. 아래 예제는 100만 개 정수를 병렬로 순회하며 각 값을 제곱으로 바꾸는데, transform과 달리 결과를 저장할 별도의 출력 범위가 필요 없어 코드가 더 간결해집니다. for_each를 병렬화할 때 가장 조심해야 할 부분은 람다 내부에서 외부 변수를 공유 상태로 건드리지 않는 것인데, 이 문제는 6절에서 자세히 다룹니다.

#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(), 
        [](int& x) {
            x = x * x;  // 제곱
        });
    
    std::cout << "v[99]: " << v[99] << std::endl;  // 10000
    
    return 0;
}

4. 병렬 집계

컨테이너의 모든 원소를 하나의 값으로 합치는 집계 연산은 언뜻 순차적으로 보이지만, std::reducestd::transform_reduce는 결합법칙이 성립하는 연산이라면 부분 합을 여러 스레드에서 독립적으로 계산한 뒤 마지막에 합치는 방식으로 병렬화합니다.

reduce

std::reducestd::accumulate의 병렬 버전이라고 볼 수 있지만, 순서를 보장하지 않는 대신 병렬·비순차 실행이 가능하다는 차이가 있습니다. 아래 예제에서는 1부터 1천만까지의 합을 구하는데, 덧셈은 결합법칙과 교환법칙이 모두 성립하는 연산이라 각 스레드가 담당 구간의 부분 합을 독립적으로 계산한 뒤 최종적으로 합쳐도 결과가 정확합니다. 반대로 뺄셈이나 나눗셈처럼 순서에 따라 결과가 달라지는 연산에는 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

std::transform_reducetransform(각 원소 변형)과 reduce(집계)를 하나의 알고리즘으로 묶은 것으로, 별도의 중간 벡터를 만들지 않고도 변형과 집계를 한 번에 병렬 수행할 수 있어 메모리 사용량과 캐시 미스를 줄여줍니다. 아래 예제에서는 각 정수를 제곱한 뒤(x * x) std::plus<>()로 합산해 제곱합을 구하는데, 이렇게 변형 함수와 결합 연산을 분리해서 전달하는 구조 덕분에 평균, 분산, 내적(dot product) 같은 다양한 집계 연산에 동일한 패턴을 재사용할 수 있습니다.

#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<>(),
        [](int x) { return static_cast<long long>(x) * x; }
    );
    
    std::cout << "제곱합: " << sum_of_squares << std::endl;
    
    return 0;
}

5. 병렬 검색

검색 알고리즘의 병렬화는 정렬이나 변환과는 조금 다른 방식으로 이득을 줍니다. 정렬된 데이터를 찾는 것이 아니라 조건에 맞는 원소를 찾을 때까지 컨테이너 전체를 훑어야 하는 선형 탐색 상황에서, 여러 스레드가 서로 다른 구간을 동시에 검색하면 전체 탐색 시간을 크게 줄일 수 있습니다.

find

std::findstd::execution::par를 지정하면 여러 스레드가 벡터의 서로 다른 구간을 동시에 탐색하다가, 어느 스레드든 목표값을 먼저 찾으면 다른 스레드의 작업을 중단시키고 결과를 반환합니다. 순차 find는 항상 앞에서부터 순서대로 탐색하지만, 병렬 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

find와 달리 count_if는 조건을 만족하는 원소를 찾는 즉시 멈추지 않고 컨테이너 전체를 끝까지 훑어 개수를 세야 하므로, 각 스레드가 담당 구간의 부분 개수를 독립적으로 세고 마지막에 합산하는 방식으로 병렬화됩니다. 아래 예제는 1천만 개의 난수 중 짝수 개수를 세는데, x % 2 == 0이라는 조건 판단이 원소마다 완전히 독립적이라 스레드 간 경쟁 없이 안전하게 병렬 실행할 수 있습니다.

#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(),
        [](int x) { return x % 2 == 0; });
    
    std::cout << "짝수 개수: " << count << std::endl;
    
    return 0;
}

6. 자주 발생하는 문제

병렬 알고리즘은 “정책 하나만 추가하면 무조건 빨라진다”는 오해를 사기 쉽지만, 실제로는 스레드 생성·동기화 비용과 공유 상태 관리라는 새로운 위험을 함께 짊어지게 됩니다. 아래 네 가지 문제는 병렬화를 적용하면서 실무에서 가장 자주 마주치는 함정입니다.

문제 1: 작은 데이터

병렬 실행에는 작업을 여러 스레드에 나누고 결과를 다시 모으는 오버헤드가 반드시 따릅니다. 데이터가 100개 수준으로 작으면 이 오버헤드가 실제 연산 시간보다 훨씬 커서, 아래 벤치마크처럼 순차 정렬이 5마이크로초 만에 끝나는 반면 병렬 정렬은 스레드 풀을 준비하는 데만 150마이크로초가 걸려 오히려 30배 느려지는 역설적인 결과가 나옵니다. 경험적으로 병렬화는 데이터가 최소 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: 공유 상태 (데이터 레이스)

병렬 알고리즘에서 가장 위험한 실수는 람다 캡처로 붙잡은 외부 변수를 여러 스레드가 동시에 수정하는 것입니다. 아래 예제처럼 sum += x를 여러 스레드가 동시에 실행하면, 읽기-더하기-쓰기라는 세 단계가 스레드 간에 뒤섞이면서 일부 덧셈 결과가 유실되는 데이터 레이스가 발생해 매 실행마다 다른(대개는 더 작은) 값이 나옵니다. 이런 상황에서는 직접 뮤텍스나 원자적 변수로 동기화를 구현하기보다, 애초에 이런 집계 목적을 위해 설계된 std::reduce를 사용하는 것이 훨씬 간단하고 안전합니다.

#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: 예외 처리

병렬로 실행되는 여러 스레드에서 동시에 예외가 발생할 수 있다는 점은 순차 코드에서는 겪을 일이 없던 새로운 복잡도입니다. 표준은 병렬 알고리즘 실행 중 발생한 예외 중 구현이 선택한 하나만 호출자에게 전파하도록 규정하고, 만약 어떤 이유로든 예외가 전파되지 못하면 std::terminate가 호출되어 프로그램이 즉시 종료될 수 있습니다. 따라서 병렬 알고리즘에 넘기는 함수 내부에서는 예외를 던지는 대신, 에러를 값으로 표현하거나 std::atomic<bool> 플래그로 실패를 기록하고 루프 밖에서 처리하는 방식이 더 안전합니다.

#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(), [](int x) {
            if (x < 0) {
                throw std::runtime_error("음수 발견");
            }
        });
    } catch (const std::exception& e) {
        // 여러 스레드에서 예외 발생 가능
        // 첫 번째 예외만 잡힘
        std::cout << "예외: " << e.what() << std::endl;
    }
    
    return 0;
}

문제 4: 순서 의존 연산

병렬 알고리즘은 각 원소에 대한 연산이 서로 독립적이라는 전제 위에서 동작합니다. 아래 첫 번째 예제처럼 모든 스레드가 v[0]이라는 동일한 원소를 동시에 읽으면서 동시에 다른 인덱스에 쓰는 코드는, v[0]이 다른 스레드에 의해 이미 변경된 시점과 아직 변경되지 않은 시점이 뒤섞여 결과를 예측할 수 없게 만듭니다. 반면 두 번째 예제처럼 각 원소가 오직 자기 자신의 이전 값만 사용해 새 값을 계산하는 x = x * 2 같은 연산은 원소 간 의존성이 전혀 없어 병렬화해도 항상 동일하고 올바른 결과를 보장합니다.

#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(), [](int& x) {
        x = x * 2;  // 각 요소 독립적
    });
    
    for (int x : v) std::cout << x << " ";
    std::cout << std::endl;
    
    return 0;
}

7. 지원 알고리즘

C++17은 <execution> 정책을 지원하는 알고리즘을 기존 <algorithm>·<numeric> 헤더 전반에 걸쳐 대폭 확장했습니다. 아래 목록은 실무에서 자주 쓰이는 병렬 지원 알고리즘을 정렬·검색·변환·집계·복사 카테고리별로 정리한 것으로, 기존에 순차로 쓰던 알고리즘 호출 앞에 정책 인자 하나만 추가하면 그대로 병렬 버전으로 전환할 수 있다는 사실을 한눈에 보여줍니다.

주요 알고리즘

이 목록의 알고리즘들은 모두 첫 번째 인자로 실행 정책을 받는다는 공통점이 있어, 프로젝트 전체에서 일관된 방식으로 병렬화를 적용할 수 있습니다. 실무에서는 std::sort, std::transform, std::reduce를 가장 많이 사용하게 되지만, std::binary_searchstd::all_of/std::any_of처럼 조건 검사 계열 알고리즘도 대용량 데이터를 다룰 때 유용하게 쓰입니다.

#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. 실전 예제: 이미지 처리

이미지 처리는 병렬 알고리즘이 진가를 발휘하는 대표적인 실무 사례입니다. 1920x1080 해상도의 이미지만 해도 200만 개가 넘는 픽셀이 있고, 그레이스케일 변환이나 밝기 조정처럼 각 픽셀을 독립적으로 처리하는 연산이 많아 병렬화 적합도가 매우 높습니다. 아래 ImageProcessor 클래스는 그레이스케일 변환, 밝기 조정, 블러, 밝은 픽셀 카운트라는 네 가지 연산을 모두 std::execution::par로 병렬화해, 각 연산이 왜 안전하게 병렬화될 수 있는지를 보여줍니다. toGrayscaleadjustBrightness는 각 픽셀이 자기 자신의 값만으로 결과를 계산하므로 완전히 독립적이고, blur는 원본 픽셀 배열(original)을 미리 복사해두고 그 복사본만 읽으면서 결과는 별도의 pixels에 쓰기 때문에 이웃 픽셀을 참조하면서도 데이터 레이스 없이 병렬화할 수 있습니다.

#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(),
            [](Pixel& p) {
                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 = [](int val) {
                    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;
}

정리

핵심 요약

  1. 병렬 알고리즘: C++17 멀티코어 활용
  2. Execution Policy: seq, par, par_unseq
  3. 성능: 큰 데이터에서 2-4배 향상
  4. 주의: 데이터 레이스, 순서 의존 금지
  5. 적용: 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 사용)

다음 단계


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Everything about C++ Parallel Algorithms : principles, complexity, implementation.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, parallel, 알고리즘, performance, C++17 등으로 검색하시면 이 글이 도움이 됩니다.