web-dev-qa-db-ja.com

並列ソートアルゴリズム

_List<T>_または配列を操作でき、場合によっては並列拡張を使用できる、C#での並列化(マルチスレッド)ソートアルゴリズムの単純な実装を探していますが、その部分は厳密には必要ありません。

編集:Frank Kruegerが良い答えを提供しますが、その例をLINQを使用しない例に変換したいと思います。また、Parallel.Do()Parallel.Invoke()に取って代わられたようです。

ありがとう。

25
redcalx

彼の記事の「TheDarkside」から 。Net Frameworkの並列拡張 クイックソートのこの並列拡張バージョンがあります。

(編集:リンクが死んでいるので、興味のある読者は ウェイバックマシン でそれのアーカイブを見つけるかもしれません)

private void QuicksortSequential<T>(T[] arr, int left, int right) 
where T : IComparable<T>
{
    if (right > left)
    {
        int pivot = Partition(arr, left, right);
        QuicksortSequential(arr, left, pivot - 1);
        QuicksortSequential(arr, pivot + 1, right);
    }
}

private void QuicksortParallelOptimised<T>(T[] arr, int left, int right) 
where T : IComparable<T>
{
    const int SEQUENTIAL_THRESHOLD = 2048;
    if (right > left)
    {
        if (right - left < SEQUENTIAL_THRESHOLD)
        {

            QuicksortSequential(arr, left, right);
        }
        else
        {
            int pivot = Partition(arr, left, right);
            Parallel.Do(
                () => QuicksortParallelOptimised(arr, left, pivot - 1),
                () => QuicksortParallelOptimised(arr, pivot + 1, right));
        }
    }
}

アイテムの数が2048未満になると、彼は順次ソートに戻ることに注意してください。

44
Frank Krueger

Updateデュアルコアマシンで1.7倍以上の高速化を実現しました。

.NET 2.0で動作し(これを確認してください)、ThreadPool以外を使用しない並列ソーターを作成してみようと思いました。

2,000,000個の要素配列を並べ替えた結果は次のとおりです。

 Time Parallel Time Sequential 
 ------------- --------------- 
 2854 ms 5052 ms 
 2846 ms 4947 ms 
 2794 ms 4940 ms 
 ... ... 
 2815 ms 4894 ms 
 2981 ms 4991 ms 
 2832ミリ秒5053ミリ秒
 
平均:2818ミリ秒平均:4969ミリ秒
標準:66ミリ秒標準:65ミリ秒
速度:1.76x 

この環境では、1.76倍のスピードアップ(私が望んでいた最適な2倍にかなり近い)が得られました。

  1. 2,000,000個のランダムなModelオブジェクト
  2. 2つのDateTimeプロパティを比較する比較デリゲートによるオブジェクトの並べ替え。
  3. MonoJITコンパイラバージョン2.4.2.3
  4. 2.4 GHz Intel Core 2Duo上のMaxOS X 10.5.8

今回は C#でのBen WatsonのQuickSort を使用しました。彼のQuickSort内部ループを次のように変更しました。

QuickSortSequential:
    QuickSortSequential (beg, l - 1);
    QuickSortSequential (l + 1, end);

に:

QuickSortParallel:
    ManualResetEvent fin2 = new ManualResetEvent (false);
    ThreadPool.QueueUserWorkItem (delegate {
        QuickSortParallel (l + 1, end);
        fin2.Set ();
    });
    QuickSortParallel (beg, l - 1);
    fin2.WaitOne (1000000);
    fin2.Close ();

(実際、コードでは、役立つように見える少しの負荷分散を行っています。)

この並列バージョンを実行すると、配列に25,000を超えるアイテムがある場合にのみ効果があることがわかりました(ただし、最低50,000を使用すると、プロセッサの呼吸が増えるようです)。

私は私の小さなデュアルコアマシンで考えられる限り多くの改善を行いました。 8ウェイモンスターのアイデアを試してみたいです。また、この作業は、Monoを実行している小さな13インチMacBookで行われました。他の人が通常の.NET2.0インストールでどのように動作するのか興味があります。

その醜い栄光のソースコードはここで入手できます: http://www.praeclarum.org/so/psort.cs.txt 。興味があれば片付けます。

7
Frank Krueger

ここでの記録は、C#2および.Net 2 + ParallelExtensionsでコンパイルされるlamda式のないバージョンです。これは、Parallel Extensionsの独自の実装(コード2008のGoogle Summerから)を備えたMonoでも機能するはずです。

/// <summary>
/// Parallel quicksort algorithm.
/// </summary>
public class ParallelSort
{
    #region Public Static Methods

    /// <summary>
    /// Sequential quicksort.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="arr"></param>
    public static void QuicksortSequential<T>(T [] arr) where T : IComparable<T>
    {
        QuicksortSequential(arr, 0, arr.Length - 1);
    }

    /// <summary>
    /// Parallel quicksort
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="arr"></param>
    public static void QuicksortParallel<T>(T[] arr) where T : IComparable<T>
    {
        QuicksortParallel(arr, 0, arr.Length - 1);
    }

    #endregion

    #region Private Static Methods

    private static void QuicksortSequential<T>(T[] arr, int left, int right) 
        where T : IComparable<T>
    {
        if (right > left)
        {
            int pivot = Partition(arr, left, right);
            QuicksortSequential(arr, left, pivot - 1);
            QuicksortSequential(arr, pivot + 1, right);
        }
    }

    private static void QuicksortParallel<T>(T[] arr, int left, int right) 
        where T : IComparable<T>
    {
        const int SEQUENTIAL_THRESHOLD = 2048;
        if (right > left)
        {
            if (right - left < SEQUENTIAL_THRESHOLD)
            {
                QuicksortSequential(arr, left, right);
            }
            else
            {
                int pivot = Partition(arr, left, right);
                Parallel.Invoke(new Action[] { delegate {QuicksortParallel(arr, left, pivot - 1);},
                                               delegate {QuicksortParallel(arr, pivot + 1, right);}
                });
            }
        }
    }

    private static void Swap<T>(T[] arr, int i, int j)
    {
        T tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    private static int Partition<T>(T[] arr, int low, int high) 
        where T : IComparable<T>
    {
        // Simple partitioning implementation
        int pivotPos = (high + low) / 2;
        T pivot = arr[pivotPos];
        Swap(arr, low, pivotPos);

        int left = low;
        for (int i = low + 1; i <= high; i++)
        {
            if (arr[i].CompareTo(pivot) < 0)
            {
                left++;
                Swap(arr, i, left);
            }
        }

        Swap(arr, low, left);
        return left;
    }

    #endregion
}
6
redcalx

プロセッサ間でブロックが分割されている、プロセッサキャッシュのサイズに基づくマージソートが思い浮かびます。

マージ段階は、マージをnビットに分割し、各プロセッサが正しいオフセットからブロックへのブロックのマージを開始することで実行できます。

私はこれを書いてみませんでした。

(CPUキャッシュとメインRAMの相対速度は、マージソートが検出されたときのRAMとテープの相対速度からそれほど遠くないので、マージソートの使用を再開する必要があります。 )

6
Ian Ringrose

必要なリストを、使用しているプロセッサの数に応じて同じサイズのサブリストに分割し、2つの部分が完了するたびに、残りが1つだけになり、すべてのスレッドが完了するまで、それらを新しい部分にマージします。非常に単純で、自分で実装できるはずです。各スレッド内のリストの並べ替えは、既存の並べ替えアルゴリズム(ライブラリなど)を使用して実行できます。

0
KernelJ