다음을 통해 공유


PLINQ의 속도 향상

이 문서에서는 올바른 결과를 생성하면서 가능한 한 효율적인 PLINQ 쿼리를 작성하는 데 도움이 되는 정보를 제공합니다.

PLINQ의 주요 목적은 다중 코어 컴퓨터에서 쿼리 대리자를 병렬로 실행하여 LINQ to Objects 쿼리의 실행 속도를 높이기 위한 것입니다. PLINQ는 개별 대리자 간에 공유 상태가 없는 소스 컬렉션의 각 요소 처리가 독립적일 때 가장 잘 수행됩니다. 이러한 작업은 LINQ to Objects 및 PLINQ에서 흔히 볼 수 있으며, 여러 스레드에서 쉽게 실행할 수 있어 종종 "유쾌하게 병렬"이라고 합니다. 그러나 모든 쿼리가 전적으로 유쾌하게 병렬 작업으로 구성되는 것은 아닙니다. 대부분의 경우 쿼리에는 병렬 처리할 수 없거나 병렬 실행 속도가 느려지는 일부 연산자가 포함됩니다. 또한 완전히 유쾌하게 병렬인 쿼리가 있더라도 PLINQ는 여전히 데이터 원본을 분할하고 스레드에서 작업을 예약해야 하며, 일반적으로 쿼리가 완료되면 결과를 병합해야 합니다. 이러한 모든 작업은 병렬 처리의 계산 비용에 추가됩니다. 이러한 병렬 처리 추가 비용을 오버헤드라고 합니다. PLINQ 쿼리에서 최적의 성능을 달성하기 위해 유쾌하게 병렬되는 부분을 최대화하고 오버헤드가 필요한 부분을 최소화하는 것이 목표입니다.

PLINQ 쿼리 성능에 영향을 주는 요소

다음 섹션에서는 병렬 쿼리 성능에 영향을 주는 가장 중요한 요소 중 일부를 나열합니다. 이는 모든 경우에 쿼리 성능을 예측하기에 충분하지 않은 일반적인 문입니다. 언제나처럼 다양한 대표 구성 및 로드가 있는 컴퓨터에서 특정 쿼리의 실제 성능을 측정하는 것이 중요합니다.

  1. 전체 작업의 계산 비용입니다.

    속도를 높이려면 PLINQ 쿼리에 오버헤드를 상쇄할 수 있을 만큼 충분한 병렬 작업이 필요합니다. 이 작업은 각 대리자의 계산 비용과 원본 컬렉션의 요소 수를 곱한 것으로 표현할 수 있습니다. 작업을 병렬 처리할 수 있다고 가정하면 계산 비용이 많이 들수록 속도 향상 기회가 커집니다. 예를 들어 함수를 실행하는 데 1밀리초가 걸리는 경우 1,000개가 넘는 요소에 대한 순차 쿼리는 해당 작업을 수행하는 데 1초가 걸리는 반면, 코어가 4개인 컴퓨터의 병렬 쿼리는 250밀리초밖에 걸리지 않을 수 있습니다. 이렇게 하면 750밀리초의 속도 향상이 생성됩니다. 함수가 각 요소에 대해 실행하는 데 1초가 필요한 경우 속도는 750초입니다. 대리자가 매우 많은 리소스를 소모하는 경우, 원본 컬렉션에 몇 가지 항목만 있어도 PLINQ는 상당한 처리 속도 향상을 제공할 수 있습니다. 반대로, 간단한 대리자가 있는 작은 소스 컬렉션은 일반적으로 PLINQ에 적합하지 않습니다.

    다음 예제에서 queryA는 Select 함수에 많은 작업이 포함되는 것으로 가정하여 PLINQ에 적합한 후보일 수 있습니다. Select 문에 충분한 작업이 없고 병렬 처리 오버헤드가 속도의 대부분 또는 전부를 오프셋하므로 queryB는 적합한 후보가 아닐 수 있습니다.

    Dim queryA = From num In numberList.AsParallel()  
                 Select ExpensiveFunction(num); 'good for PLINQ  
    
    Dim queryB = From num In numberList.AsParallel()  
                 Where num Mod 2 > 0  
                 Select num; 'not as good for PLINQ  
    
    var queryA = from num in numberList.AsParallel()  
                 select ExpensiveFunction(num); //good for PLINQ  
    
    var queryB = from num in numberList.AsParallel()  
                 where num % 2 > 0  
                 select num; //not as good for PLINQ  
    
  2. 시스템의 논리 코어 수(병렬 처리 수준)입니다.

    이 점은 이전 섹션에 대한 명백한 결과이며, 작업이 더 많은 동시 스레드 간에 분할될 수 있기 때문에 더 많은 코어가 있는 컴퓨터에서 유쾌하게 병렬로 실행되는 쿼리가 더 빠릅니다. 전체 속도 향상은 쿼리의 전체 작업에서 병렬 처리할 수 있는 비율에 따라 달라집니다. 그러나 모든 쿼리가 8개의 코어 컴퓨터에서 4코어 컴퓨터보다 두 배 빠른 속도로 실행된다고 가정하지는 않습니다. 최적의 성능을 위해 쿼리를 튜닝할 때는 다양한 수의 코어가 있는 컴퓨터에서 실제 결과를 측정하는 것이 중요합니다. 이 점은 포인트 #1과 관련이 있습니다. 더 큰 컴퓨팅 리소스를 활용하려면 더 큰 데이터 세트가 필요합니다.

  3. 작업의 수와 종류입니다.

    PLINQ는 소스 시퀀스에서 요소의 순서를 유지해야 하는 상황에 대해 AsOrdered 연산자를 제공합니다. 주문과 관련된 비용이 있지만 이 비용은 일반적으로 적습니다. GroupBy 및 조인 작업도 마찬가지로 오버헤드가 발생합니다. PLINQ는 소스 컬렉션의 요소를 순서대로 처리하고 준비되는 즉시 다음 연산자에 전달할 수 있는 경우에 가장 잘 수행됩니다. 자세한 내용은 PLINQ의 Order 유지를 참조하세요.

  4. 쿼리 실행 형식입니다.

    ToArray 또는 ToList를 호출하여 쿼리 결과를 저장하는 경우 모든 병렬 스레드의 결과를 단일 데이터 구조로 병합해야 합니다. 여기에는 피할 수 없는 계산 비용이 포함됩니다. 마찬가지로 foreach(For Each in Visual Basic) 루프를 사용하여 결과를 반복하는 경우 작업자 스레드의 결과를 열거자 스레드로 직렬화해야 합니다. 그러나 각 스레드의 결과에 따라 일부 작업을 수행하려는 경우 ForAll 메서드를 사용하여 여러 스레드에서 이 작업을 수행할 수 있습니다.

  5. 병합 옵션의 형식입니다.

    PLINQ는 출력을 버퍼링하고 청크로 생성하거나 전체 결과 집합이 생성된 후 한 번에 생성하거나 생성될 때 개별 결과를 스트리밍하도록 구성할 수 있습니다. 전자는 전체 실행 시간이 감소하고 후자는 생성된 요소 간의 대기 시간이 감소합니다. 병합 옵션이 항상 전체 쿼리 성능에 큰 영향을 주는 것은 아니지만 사용자가 결과를 보기 위해 기다려야 하는 시간을 제어하기 때문에 인식된 성능에 영향을 미칠 수 있습니다. 자세한 내용은 PLINQ의 병합 옵션을 참조하세요.

  6. 분할의 종류

    경우에 따라 인덱싱 가능한 원본 컬렉션에 대한 PLINQ 쿼리로 인해 불균형한 작업 부하가 발생할 수 있습니다. 이 경우 사용자 지정 파티셔너를 만들어 쿼리 성능을 높일 수 있습니다. 자세한 내용은 PLINQ 및 TPL에 대한 사용자 지정 파티셔너를 참조하세요.

PLINQ가 순차 모드를 선택하는 경우

PLINQ는 항상 쿼리가 순차적으로 실행되는 속도만큼 빠르게 쿼리를 실행하려고 시도합니다. PLINQ는 사용자 대리자의 계산 비용이 얼마나 드는지 또는 입력 원본의 크기가 얼마나 큰지 않지만 특정 쿼리 "셰이프"를 찾습니다. 특히 일반적으로 쿼리가 병렬 모드에서 더 느리게 실행되도록 하는 쿼리 연산자 또는 연산자의 조합을 찾습니다. 이러한 셰이프를 찾으면 기본적으로 PLINQ는 순차 모드로 돌아갑니다.

그러나 특정 쿼리의 성능을 측정한 후 실제로 병렬 모드에서 더 빠르게 실행되는지 확인할 수 있습니다. 이러한 경우 ParallelExecutionMode.ForceParallelism 메서드를 통해 WithExecutionMode 플래그를 사용하여 PLINQ에 쿼리를 병렬화하도록 지시할 수 있습니다. 자세한 내용은 방법: PLINQ에서 실행 모드 지정을 참조하세요.

다음 목록에서는 PLINQ가 기본적으로 순차 모드에서 실행하는 쿼리 셰이프를 설명합니다.

  • 원래 인덱스를 제거하거나 다시 정렬한 순서 지정 또는 필터링 연산자 뒤에 Select, 인덱싱된 위치, 인덱싱된 SelectMany 또는 ElementAt 절이 포함된 쿼리입니다.

  • Take, TakeWhile, Skip, SkipWhile 연산자를 포함하고 원본 시퀀스의 인덱스가 원래 순서가 아닌 쿼리입니다.

  • 데이터 원본 중 하나에 원래 정렬된 인덱스가 있고 다른 데이터 원본이 배열이나 IList(T)처럼 인덱싱할 수 있는 경우를 제외하고, Zip 또는 SequenceEquals를 포함하는 쿼리.

  • 인덱싱 가능한 데이터 원본에 적용되지 않는 한 Concat을 포함하는 쿼리입니다.

  • 인덱싱 가능한 데이터 원본에 적용되지 않는 한 역방향이 포함된 쿼리입니다.

참고하십시오