방법: parallel_invoke를 사용하여 병렬 정렬 루틴 작성

업데이트: 2010년 7월

이 문서에서는 parallel_invoke 알고리즘을 사용하여 바이토닉 정렬 알고리즘의 성능을 향상시키는 방법에 대해 설명합니다. 바이토닉 정렬 알고리즘은 입력 시퀀스를 더 작은 정렬된 파티션으로 재귀적으로 나눕니다. 각 파티션 작업은 다른 모든 작업과 별개로 실행되므로 바이토닉 정렬 알고리즘을 병렬로 실행할 수 있습니다.

바이토닉 정렬은 입력 시퀀스의 모든 조합을 정렬하는 정렬 네트워크의 한 예라고 할 수 있지만 이 방법은 길이가 2의 거듭제곱인 시퀀스를 정렬합니다.

단원

이 문서에서는 다음 작업에 대해 설명합니다.

  • 직렬 바이토닉 정렬 수행

  • parallel_invoke를 사용하여 바이토닉 정렬을 병렬로 수행

직렬 바이토닉 정렬 수행

다음 예제에서는 직렬 버전의 바이토닉 정렬 알고리즘을 보여 줍니다. bitonic_sort 함수는 시퀀스를 두 개의 파티션으로 나누고, 이러한 파티션을 반대 방향으로 정렬한 후 결과를 병합합니다. 이 함수는 자신을 재귀적으로 두 번 호출하여 각 파티션을 정렬합니다.

const bool INCREASING = true;
const bool DECREASING = false;

// Comparator function for the bitonic sort algorithm.
template <class T>
void compare(T* items, int i, int j, bool dir)
{
   if (dir == (items[i] > items[j]))
   {
      swap(items[i], items[j]);
   }
}

// Sorts a bitonic sequence in the specified order.
template <class T>
void bitonic_merge(T* items, int lo, int n, bool dir)
{
   if (n > 1)
   {
      int m = n / 2;
      for (int i = lo; i < lo + m; ++i)
      {
         compare(items, i, i + m, dir);
      }
      bitonic_merge(items, lo, m, dir);
      bitonic_merge(items, lo + m, m, dir);
   }
}

// Sorts the given sequence in the specified order.
template <class T>
void bitonic_sort(T* items, int lo, int n, bool dir)
{
   if (n > 1)
   {
      // Divide the array into two partitions and then sort 
      // the partitions in different directions.
      int m = n / 2;
      bitonic_sort(items, lo, m, INCREASING);
      bitonic_sort(items, lo + m, m, DECREASING);

      // Merge the results.
      bitonic_merge(items,lo, n, dir);
   }
}

// Sorts the given sequence in increasing order.
template <class T>
void bitonic_sort(T* items, int size)
{
    bitonic_sort(items, 0, size, INCREASING);
}

[맨 위로 이동]

parallel_invoke를 사용하여 바이토닉 정렬을 병렬로 수행

이 단원에서는 parallel_invoke 알고리즘을 사용하여 바이토닉 정렬 알고리즘을 병렬로 수행하는 방법에 대해 설명합니다.

절차

바이토닉 정렬 알고리즘을 병렬로 수행하려면

  1. 헤더 파일 ppl.h에 대한 #include 지시문을 추가합니다.

    #include <ppl.h>
    
  2. Concurrency 네임스페이스에 대한 using 지시문을 추가합니다.

    using namespace Concurrency;
    
  3. parallel_bitonic_mege라는 새 함수를 만듭니다. 이 함수는 작업량이 충분할 경우 parallel_invoke 알고리즘을 사용하여 시퀀스를 병렬로 병합합니다. 그렇지 않으면 bitonic_merge를 호출하여 시퀀스를 연속으로 병합합니다.

    // Sorts a bitonic sequence in the specified order.
    template <class T>
    void parallel_bitonic_merge(T* items, int lo, int n, bool dir)
    {   
       // Merge the sequences concurrently if there is sufficient work to do.
       if (n > 500)
       {
          int m = n / 2;
          for (int i = lo; i < lo + m; ++i)
          {
             compare(items, i, i + m, dir);
          }
    
          // Use the parallel_invoke algorithm to merge the sequences in parallel.
          parallel_invoke(
             [&items,lo,m,dir] { parallel_bitonic_merge(items, lo, m, dir); },
             [&items,lo,m,dir] { parallel_bitonic_merge(items, lo + m, m, dir); }
          );
       }
       // Otherwise, perform the work serially.
       else if (n > 1)
       {
          bitonic_merge(items, lo, n, dir);
       }   
    }
    
  4. bitonic_sort 함수에 대해 이전 단계와 유사한 프로세스를 수행합니다.

    // Sorts the given sequence in the specified order.
    template <class T>
    void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
    {   
       if (n > 1)
       {
          // Divide the array into two partitions and then sort 
          // the partitions in different directions.
          int m = n / 2;
    
          // Sort the partitions in parallel.
          parallel_invoke(
             [&items,lo,m] { parallel_bitonic_sort(items, lo, m, INCREASING); },
             [&items,lo,m] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
          );
    
          // Merge the results.
          parallel_bitonic_merge(items, lo, n, dir);
       }
    }
    
  5. 배열을 오름차순으로 정렬하는 parallel_bitonic_sort 함수의 오버로드된 버전을 만듭니다.

    // Sorts the given sequence in increasing order.
    template <class T>
    void parallel_bitonic_sort(T* items, int size)
    {
       parallel_bitonic_sort(items, 0, size, INCREASING);
    }
    

parallel_invoke 알고리즘은 일련의 작업 중 마지막 작업을 호출 컨텍스트에서 수행하여 오버헤드를 줄입니다. 예를 들어 parallel_bitonic_sort 함수에서 첫 번째 작업은 별도의 컨텍스트에서 실행되고 두 번째 작업은 호출 컨텍스트에서 실행됩니다.

// Sort the partitions in parallel.
parallel_invoke(
   [&items,lo,m] { parallel_bitonic_sort(items, lo, m, INCREASING); },
   [&items,lo,m] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
);

다음 전체 예제에서는 바이토닉 정렬 알고리즘의 직렬 버전과 병렬 버전을 둘 다 수행합니다. 또한 각 계산을 수행하는 데 필요한 시간을 콘솔에 출력합니다.

// parallel-bitonic-sort.cpp
// compile with: /EHsc
#include <windows.h>
#include <algorithm>
#include <iostream>
#include <random>
#include <ppl.h>

using namespace Concurrency;
using namespace std;

// Calls the provided work function and returns the number of milliseconds 
// that it takes to call that function.
template <class Function>
__int64 time_call(Function&& f)
{
   __int64 begin = GetTickCount();
   f();
   return GetTickCount() - begin;
}

const bool INCREASING = true;
const bool DECREASING = false;

// Comparator function for the bitonic sort algorithm.
template <class T>
void compare(T* items, int i, int j, bool dir)
{
   if (dir == (items[i] > items[j]))
   {
      swap(items[i], items[j]);
   }
}

// Sorts a bitonic sequence in the specified order.
template <class T>
void bitonic_merge(T* items, int lo, int n, bool dir)
{
   if (n > 1)
   {
      int m = n / 2;
      for (int i = lo; i < lo + m; ++i)
      {
         compare(items, i, i + m, dir);
      }
      bitonic_merge(items, lo, m, dir);
      bitonic_merge(items, lo + m, m, dir);
   }
}

// Sorts the given sequence in the specified order.
template <class T>
void bitonic_sort(T* items, int lo, int n, bool dir)
{
   if (n > 1)
   {
      // Divide the array into two partitions and then sort 
      // the partitions in different directions.
      int m = n / 2;
      bitonic_sort(items, lo, m, INCREASING);
      bitonic_sort(items, lo + m, m, DECREASING);

      // Merge the results.
      bitonic_merge(items,lo, n, dir);
   }
}

// Sorts the given sequence in increasing order.
template <class T>
void bitonic_sort(T* items, int size)
{
    bitonic_sort(items, 0, size, INCREASING);
}

// Sorts a bitonic sequence in the specified order.
template <class T>
void parallel_bitonic_merge(T* items, int lo, int n, bool dir)
{   
   // Merge the sequences concurrently if there is sufficient work to do.
   if (n > 500)
   {
      int m = n / 2;
      for (int i = lo; i < lo + m; ++i)
      {
         compare(items, i, i + m, dir);
      }

      // Use the parallel_invoke algorithm to merge the sequences in parallel.
      parallel_invoke(
         [&items,lo,m,dir] { parallel_bitonic_merge(items, lo, m, dir); },
         [&items,lo,m,dir] { parallel_bitonic_merge(items, lo + m, m, dir); }
      );
   }
   // Otherwise, perform the work serially.
   else if (n > 1)
   {
      bitonic_merge(items, lo, n, dir);
   }   
}

// Sorts the given sequence in the specified order.
template <class T>
void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
{   
   if (n > 1)
   {
      // Divide the array into two partitions and then sort 
      // the partitions in different directions.
      int m = n / 2;

      // Sort the partitions in parallel.
      parallel_invoke(
         [&items,lo,m] { parallel_bitonic_sort(items, lo, m, INCREASING); },
         [&items,lo,m] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
      );

      // Merge the results.
      parallel_bitonic_merge(items, lo, n, dir);
   }
}

// Sorts the given sequence in increasing order.
template <class T>
void parallel_bitonic_sort(T* items, int size)
{
   parallel_bitonic_sort(items, 0, size, INCREASING);
}

int wmain()
{  
   // For this example, the size must be a power of two.
   const int size = 0x200000;

   // Create two large arrays and fill them with random values.
   int* a1 = new int[size];
   int* a2 = new int[size];

   mt19937 gen(42);
   for(int i = 0; i < size; ++i)
   {
      a1[i] = a2[i] = gen();
   }

   __int64 elapsed;

   // Perform the serial version of the sort.
   elapsed = time_call([&] { bitonic_sort(a1, size); });
   wcout << L"serial time: " << elapsed << endl;

   // Now perform the parallel version of the sort.
   elapsed = time_call([&] { parallel_bitonic_sort(a2, size); });
   wcout << L"parallel time: " << elapsed << endl;

   delete[] a1;
   delete[] a2;
}

다음 샘플은 프로세서가 4개인 컴퓨터에 대한 출력입니다.

serial time: 4353
parallel time: 1248

[맨 위로 이동]

코드 컴파일

코드를 컴파일하려면 코드를 복사한 다음, Visual Studio 프로젝트 또는 parallel-bitonic-sort.cpp 파일에 붙여넣고 Visual Studio 명령 프롬프트 창에서 다음 명령을 실행합니다.

cl.exe /EHsc parallel-bitonic-sort.cpp

강력한 프로그래밍

이 예제에서는 각 작업 그룹의 수명이 함수보다 짧기 때문에 Concurrency::task_group 클래스 대신 parallel_invoke 알고리즘을 사용합니다. 가능하면, task group 개체보다 실행 오버헤드가 적어서 성능이 높은 코드를 작성할 수 있는 parallel_invoke를 사용하는 것이 좋습니다.

일부 알고리즘의 병렬 버전은 수행할 작업의 양이 충분한 경우에만 효과적으로 수행됩니다. 예를 들어 parallel_bitonic_merge 함수는 시퀀스의 요소가 500개 이하인 경우 직렬 버전 bitonic_merge를 호출합니다. 작업량을 기반으로 전체 정렬 방법을 계획할 수도 있습니다. 예를 들어 배열에 포함되어 있는 항목이 500개 미만인 경우 다음 예제에서와 같이 직렬 버전의 빠른 정렬 알고리즘을 사용하는 것이 더 효율적일 수 있습니다.

template <class T>
void quick_sort(T* items, int lo, int n)
{
   // TODO: The function body is omitted for brevity.
}

template <class T>
void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
{
   // Use the serial quick sort algorithm if there are relatively few
   // items to sort. The associated overhead for running few tasks in 
   // parallel may not overcome the benefits of parallel processing.
   if (n - lo + 1 <= 500)
   {
      quick_sort(items, lo, n);
   }
   else if (n > 1)
   {
      // Divide the array into two partitions and then sort 
      // the partitions in different directions.
      int m = n / 2;

      // Sort the partitions in parallel.
      parallel_invoke(
         [&items,lo,m] { parallel_bitonic_sort(items, lo, m, INCREASING); },
         [&items,lo,m] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
      );

      // Merge the results.
      parallel_bitonic_merge(items, lo, n, dir);
   }
}

병렬 알고리즘과 마찬가지로 필요에 따라 코드를 프로파일링하고 조정하는 것이 좋습니다.

참고 항목

참조

parallel_invoke 함수

개념

작업 병렬 처리(동시성 런타임)

변경 기록

날짜

변경 내용

이유

2010년 7월

parallel_invoke를 사용하는 예제를 업데이트했습니다.

향상된 기능 관련 정보