コンテスト:ガウス分布データの大きな配列を並べ替える最速の方法


71

この質問への関心に続いて、コンテストを提案することで、回答をもう少し客観的かつ定量的にすることは興味深いと思いました。

アイデアは簡単です:5000万ガウス分布のdoubleを含むバイナリファイルを生成しました(平均:0、stdev 1)。目標は、メモリ内でこれらをできるだけ速くソートするプログラムを作成することです。Pythonでの非常に単純なリファレンス実装は、完了するのに1m4sかかります。どのくらい低くすることができますか?

ルールは次のとおりです。ファイル「gaussian.dat」を開き、メモリ内の数値を並べ替えるプログラムで出力する(出力する必要はありません)。プログラムのビルドと実行の手順。このプログラムは、私のArch Linuxマシンで動作する必要があります(このシステムに簡単にインストールできるプログラミング言語またはライブラリを使用できることを意味します)。

プログラムは、安全に起動できるようにするために、合理的に読み取り可能である必要があります(アセンブラーのみのソリューションはご遠慮ください!)。

私のマシンで答えを実行します(クアッドコア、4ギガバイトのRAM)。最速の解決策は、受け入れられた答えと100ポイントの報奨金を取得します:)

数値の生成に使用されるプログラム:

#!/usr/bin/env python
import random
from array import array
from sys import argv
count=int(argv[1])
a=array('d',(random.gauss(0,1) for x in xrange(count)))
f=open("gaussian.dat","wb")
a.tofile(f)

単純なリファレンス実装:

#!/usr/bin/env python
from array import array
from sys import argv
count=int(argv[1])
a=array('d')
a.fromfile(open("gaussian.dat"),count)
print "sorting..."
b=sorted(a)

編集:4 GBのRAMのみ、ごめんなさい

EDIT#2:コンテストのポイントは、データに関する事前情報を使用できるかどうかを確認することです。異なるプログラミング言語の実装間でやりがいのある一致を想定していません!


1
各値を取得して、「期待される」位置に直接移動し、変位した値について繰り返します。それに関するいくつかの問題を解決する方法がわからない。完了したら、完了するまでバブルソートします(2、3回パスする必要があります)。

1
明日夕方にバケットソートソリューションを投稿します。それまでに閉じられていない場合は、:)

1
@static_rtti-ヘビーCGユーザーとして、これはまさに「私たち」がCG.SEでハッキングしたいものです。読書modには、これをCGに移動し、閉じないでください。
-arrdem

1
CodeGolf.SEへようこそ!これが属するかどうかに関するSOの原文から多くの解説をクリアし、CodeGolf.SEメインストリームにより近くなるようにタグを付け直しました。
dmckee

2
ここで厄介な問題の1つは、客観的な勝利基準を探し、「最速」でプラットフォームの依存関係を導入することです。cpython仮想マシンに実装されたO(n ^ {1.2})アルゴリズムがO(n ^ {1.3} )cで実装された同様の定数を持つアルゴリズム?一般に、各ソリューションのパフォーマンス特性について議論することをお勧めします。これは、人々が何が起こっているかを判断するのに役立つ可能性があるためです。
dmckee

回答:


13

次に、C ++のソリューションを示します。このソリューションでは、最初に、予想される要素の数が同じバケットに数値をパーティション分割し、次に各バケットを個別にソートします。Wikipediaのいくつかの式に基づいて累積分布関数のテーブルを事前計算し、このテーブルの値を補間して高速な近似値を取得します。

4つのコアを利用するために、複数のスレッドでいくつかのステップが実行されます。

#include <cstdlib>
#include <math.h>
#include <stdio.h>
#include <algorithm>

#include <tbb/parallel_for.h>

using namespace std;

typedef unsigned long long ull;

double signum(double x) {
    return (x<0) ? -1 : (x>0) ? 1 : 0;
}

const double fourOverPI = 4 / M_PI;

double erf(double x) {
    double a = 0.147;
    double x2 = x*x;
    double ax2 = a*x2;
    double f1 = -x2 * (fourOverPI + ax2) / (1 + ax2);
    double s1 = sqrt(1 - exp(f1));
    return signum(x) * s1;
}

const double sqrt2 = sqrt(2);

double cdf(double x) {
    return 0.5 + erf(x / sqrt2) / 2;
}

const int cdfTableSize = 200;
const double cdfTableLimit = 5;
double* computeCdfTable(int size) {
    double* res = new double[size];
    for (int i = 0; i < size; ++i) {
        res[i] = cdf(cdfTableLimit * i / (size - 1));
    }
    return res;
}
const double* const cdfTable = computeCdfTable(cdfTableSize);

double cdfApprox(double x) {
    bool negative = (x < 0);
    if (negative) x = -x;
    if (x > cdfTableLimit) return negative ? cdf(-x) : cdf(x);
    double p = (cdfTableSize - 1) * x / cdfTableLimit;
    int below = (int) p;
    if (p == below) return negative ? -cdfTable[below] : cdfTable[below];
    int above = below + 1;
    double ret = cdfTable[below] +
            (cdfTable[above] - cdfTable[below])*(p - below);
    return negative ? 1 - ret : ret;
}

void print(const double* arr, int len) {
    for (int i = 0; i < len; ++i) {
        printf("%e; ", arr[i]);
    }
    puts("");
}

void print(const int* arr, int len) {
    for (int i = 0; i < len; ++i) {
        printf("%d; ", arr[i]);
    }
    puts("");
}

void fillBuckets(int N, int bucketCount,
        double* data, int* partitions,
        double* buckets, int* offsets) {
    for (int i = 0; i < N; ++i) {
        ++offsets[partitions[i]];
    }

    int offset = 0;
    for (int i = 0; i < bucketCount; ++i) {
        int t = offsets[i];
        offsets[i] = offset;
        offset += t;
    }
    offsets[bucketCount] = N;

    int next[bucketCount];
    memset(next, 0, sizeof(next));
    for (int i = 0; i < N; ++i) {
        int p = partitions[i];
        int j = offsets[p] + next[p];
        ++next[p];
        buckets[j] = data[i];
    }
}

class Sorter {
public:
    Sorter(double* data, int* offsets) {
        this->data = data;
        this->offsets = offsets;
    }

    static void radixSort(double* arr, int len) {
        ull* encoded = (ull*)arr;
        for (int i = 0; i < len; ++i) {
            ull n = encoded[i];
            if (n & signBit) {
                n ^= allBits;
            } else {
                n ^= signBit;
            }
            encoded[i] = n;
        }

        const int step = 11;
        const ull mask = (1ull << step) - 1;
        int offsets[8][1ull << step];
        memset(offsets, 0, sizeof(offsets));

        for (int i = 0; i < len; ++i) {
            for (int b = 0, j = 0; b < 64; b += step, ++j) {
                int p = (encoded[i] >> b) & mask;
                ++offsets[j][p];
            }
        }

        int sum[8] = {0};
        for (int i = 0; i <= mask; i++) {
            for (int b = 0, j = 0; b < 64; b += step, ++j) {
                int t = sum[j] + offsets[j][i];
                offsets[j][i] = sum[j];
                sum[j] = t;
            }
        }

        ull* copy = new ull[len];
        ull* current = encoded;
        for (int b = 0, j = 0; b < 64; b += step, ++j) {
            for (int i = 0; i < len; ++i) {
                int p = (current[i] >> b) & mask;
                copy[offsets[j][p]] = current[i];
                ++offsets[j][p];
            }

            ull* t = copy;
            copy = current;
            current = t;
        }

        if (current != encoded) {
            for (int i = 0; i < len; ++i) {
                encoded[i] = current[i];
            }
        }

        for (int i = 0; i < len; ++i) {
            ull n = encoded[i];
            if (n & signBit) {
                n ^= signBit;
            } else {
                n ^= allBits;
            }
            encoded[i] = n;
        }
    }

    void operator() (tbb::blocked_range<int>& range) const {
        for (int i = range.begin(); i < range.end(); ++i) {
            double* begin = &data[offsets[i]];
            double* end = &data[offsets[i+1]];
            //std::sort(begin, end);
            radixSort(begin, end-begin);
        }
    }

private:
    double* data;
    int* offsets;
    static const ull signBit = 1ull << 63;
    static const ull allBits = ~0ull;
};

void sortBuckets(int bucketCount, double* data, int* offsets) {
    Sorter sorter(data, offsets);
    tbb::blocked_range<int> range(0, bucketCount);
    tbb::parallel_for(range, sorter);
    //sorter(range);
}

class Partitioner {
public:
    Partitioner(int bucketCount, double* data, int* partitions) {
        this->data = data;
        this->partitions = partitions;
        this->bucketCount = bucketCount;
    }

    void operator() (tbb::blocked_range<int>& range) const {
        for (int i = range.begin(); i < range.end(); ++i) {
            double d = data[i];
            int p = (int) (cdfApprox(d) * bucketCount);
            partitions[i] = p;
        }
    }

private:
    double* data;
    int* partitions;
    int bucketCount;
};

const int bucketCount = 512;
int offsets[bucketCount + 1];

int main(int argc, char** argv) {
    if (argc != 2) {
        printf("Usage: %s N\n N = the size of the input\n", argv[0]);
        return 1;
    }

    puts("initializing...");
    int N = atoi(argv[1]);
    double* data = new double[N];
    double* buckets = new double[N];
    memset(offsets, 0, sizeof(offsets));
    int* partitions = new int[N];

    puts("loading data...");
    FILE* fp = fopen("gaussian.dat", "rb");
    if (fp == 0 || fread(data, sizeof(*data), N, fp) != N) {
        puts("Error reading data");
        return 1;
    }
    //print(data, N);

    puts("assigning partitions...");
    tbb::parallel_for(tbb::blocked_range<int>(0, N),
            Partitioner(bucketCount, data, partitions));

    puts("filling buckets...");
    fillBuckets(N, bucketCount, data, partitions, buckets, offsets);
    data = buckets;

    puts("sorting buckets...");
    sortBuckets(bucketCount, data, offsets);

    puts("done.");

    /*
    for (int i = 0; i < N-1; ++i) {
        if (data[i] > data[i+1]) {
            printf("error at %d: %e > %e\n", i, data[i], data[i+1]);
        }
    }
    */

    //print(data, N);

    return 0;
}

コンパイルして実行するには、次のコマンドを使用します。

g++ -O3 -ltbb -o gsort gsort.cpp && time ./gsort 50000000

編集:すべてのバケットが同じ配列に配置されるようになり、バケットをコピーして配列に戻す必要がなくなりました。また、値が十分に正確であるため、事前に計算された値を持つテーブルのサイズが削減されました。それでも、バケット数を256を超えて変更すると、プログラムはその数のバケットよりも実行に時間がかかります。

編集:同じアルゴリズム、異なるプログラミング言語。Javaの代わりにC ++を使用し、実行時間をマシンで〜3.2秒から〜2.35秒に短縮しました。バケットの最適な数はまだ約256です(これも私のコンピューターでは)。

ところで、tbbは本当に素晴らしいです。

編集: Alexandruの素晴らしいソリューションに触発され、最終段階のstd :: sortを彼の基数ソートの修正バージョンに置き換えました。配列をより多くのパスが必要な場合でも、別の方法を使用して正/負の数を処理しました。また、配列を正確にソートし、挿入ソートを削除することも決めました。後で、これらの変更がパフォーマンスにどのように影響し、場合によっては元に戻すかをテストするために少し時間をかけます。ただし、基数ソートを使用すると、時間は〜2.35秒から〜1.63秒に短縮されました。


いいね 私は3.055を得ました。私が手に入れることができた最低は6.3でした。統計をより良くするためにあなたのものを選んでいます。バケットの数として256を選択した理由は何ですか?128と512を試しましたが、256が最適でした。
スコット

バケットの数として256を選択したのはなぜですか?128と512を試しましたが、256が最適でした。:)私はそれを経験的に見つけましたが、なぜバケットの数を増やすとアルゴリズムが遅くなるのか分かりません-メモリの割り当てはそれほど長くはかからないはずです。たぶん、キャッシュサイズに関連した何か?
k21

私のマシンの2.725秒。JVMのロード時間を考慮に入れると、Javaソリューションには非常に便利です。
-static_rtti

2
私とArjanのソリューションごとにコードを切り替えてnioパッケージを使用し(私の構文よりもきれいだったので、彼の構文を使用しました)、0.3秒高速に取得できました。私はssdを持っています。もしそうでなければどういう意味があるのでしょうか。また、多少の調整が必要です。改造されたセクションはこちらです。
スコット

3
これは、私のテスト(16コアCPU)で最速の並列ソリューションです。1.94秒の2位からは程遠い1.22秒。
アレクサンドルー

13

スマートにならずに、より高速なナイーブソーターを提供するために、Pythonのものとほぼ同等のCのものを以下に示します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cmp(const void* av, const void* bv) {
    double a = *(const double*)av;
    double b = *(const double*)bv;
    return a < b ? -1 : a > b ? 1 : 0;
}
int main(int argc, char** argv) {
    if (argc <= 1)
        return puts("No argument!");
    unsigned count = atoi(argv[1]);

    double *a = malloc(count * sizeof *a);

    FILE *f = fopen("gaussian.dat", "rb");
    if (fread(a, sizeof *a, count, f) != count)
        return puts("fread failed!");
    fclose(f);

    puts("sorting...");
    double *b = malloc(count * sizeof *b);
    memcpy(b, a, count * sizeof *b);
    qsort(b, count, sizeof *b, cmp);
    return 0;
}

でコンパイルするとgcc -O3、私のマシンではPythonよりも1分以上かかります。87秒に対して約11秒です。


1
私のマシンで10.086を取得したため、現在のリーダーになっています!しかし、私たちはもっと良くできると確信しています:)

1
2番目の3項演算子を削除して、その場合は単純に1を返してみてください。ランダムな倍数はこれらのデータ量で互いに等しくないためです。
コーディズム

@Codism:同等のデータの場所の交換は気にしないので、同等の値を取得できたとしても適切な単純化になると付け加えます。

10

標準偏差に基づいて4分の1に分割するのが最適なセグメントに分割しました。編集:http://en.wikipedia.org/wiki/Error_function#Table_of_valuesの x値に基づいてパーティションに書き換えられました

http://www.wolframalpha.com/input/?i=percentages+by++normal+distribution

小さいバケットを使用してみましたが、使用可能なコアの数を超えて2 *の効果はほとんどありませんでした。並列コレクションがない場合、ボックスでは37秒かかり、並列コレクションでは24秒かかります。ディストリビューションを介してパーティショニングする場合、配列を使用することはできないため、さらにオーバーヘッドがかかります。Scalaで値がボックス化/ボックス化解除されるタイミングは明確ではありません。

並列コレクションにscala 2.9を使用しています。tar.gzディストリビューションをダウンロードするだけです。

コンパイルするには:scalac SortFile.scala(scala / binフォルダーに直接コピーしました。

実行するには:JAVA_OPTS = "-Xmx4096M" ./scala SortFile(2ギガバイトのRAMで実行し、ほぼ同じ時間になりました)

編集:allocateDirectを削除しました。割り当てよりも遅くなりました。配列バッファーの初期サイズのプライミングを削除しました。実際には、50000000の値全体を読み取らせました。うまくいけばオートボクシングの問題を回避するために書き直しました(ナイーブcよりもまだ遅いです)

import java.io.FileInputStream;
import java.nio.ByteBuffer
import java.nio.ByteOrder
import scala.collection.mutable.ArrayBuilder


object SortFile {

//used partition numbers from Damascus' solution
val partList = List(0, 0.15731, 0.31864, 0.48878, 0.67449, 0.88715, 1.1503, 1.5341)

val listSize = partList.size * 2;
val posZero = partList.size;
val neg = partList.map( _ * -1).reverse.zipWithIndex
val pos = partList.map( _ * 1).zipWithIndex.reverse

def partition(dbl:Double): Int = { 

//for each partition, i am running through the vals in order
//could make this a binary search to be more performant... but our list size is 4 (per side)

  if(dbl < 0) { return neg.find( dbl < _._1).get._2  }
  if(dbl > 0) { return posZero  + pos.find( dbl > _._1).get._2  }
      return posZero; 

}

  def main(args: Array[String])
    { 

    var l = 0
    val dbls = new Array[Double](50000000)
    val partList = new Array[Int](50000000)
    val pa = Array.fill(listSize){Array.newBuilder[Double]}
    val channel = new FileInputStream("../../gaussian.dat").getChannel()
    val bb = ByteBuffer.allocate(50000000 * 8)
    bb.order(ByteOrder.LITTLE_ENDIAN)
    channel.read(bb)
    bb.rewind
    println("Loaded" + System.currentTimeMillis())
    var dbl = 0.0
    while(bb.hasRemaining)
    { 
      dbl = bb.getDouble
      dbls.update(l,dbl) 

      l+=1
    }
    println("Beyond first load" + System.currentTimeMillis());

    for( i <- (0 to 49999999).par) { partList.update(i, partition(dbls(i)))}

    println("Partition computed" + System.currentTimeMillis() )
    for(i <- (0 to 49999999)) { pa(partList(i)) += dbls(i) }
    println("Partition completed " + System.currentTimeMillis())
    val toSort = for( i <- pa) yield i.result()
    println("Arrays Built" + System.currentTimeMillis());
    toSort.par.foreach{i:Array[Double] =>scala.util.Sorting.quickSort(i)};

    println("Read\t" + System.currentTimeMillis());

  }
}

1
8.185s!scalaのソリューションにはうれしいと思います...また、ガウス分布を何らかの方法で実際に使用する最初のソリューションを提供するための勇気もあります!

1
私はC#ソリューションとの競争を目指していました。私はc / c ++を破るとは思いませんでした。また..あなたと私とでは振る舞いが大きく異なります。私は自分の側でopenJDKを使用していますが、かなり遅いです。さらにパーティションを追加すると、環境に役立つのだろうかと思います。
スコット

9

これをcsファイルに入れて、理論的にcscでコンパイルします:(モノが必要です)

using System;
using System.IO;
using System.Threading;

namespace Sort
{
    class Program
    {
        const int count = 50000000;
        static double[][] doubles;
        static WaitHandle[] waiting = new WaitHandle[4];
        static AutoResetEvent[] events = new AutoResetEvent[4];

        static double[] Merge(double[] left, double[] right)
        {
            double[] result = new double[left.Length + right.Length];
            int l = 0, r = 0, spot = 0;
            while (l < left.Length && r < right.Length)
            {
                if (right[r] < left[l])
                    result[spot++] = right[r++];
                else
                    result[spot++] = left[l++];
            }
            while (l < left.Length) result[spot++] = left[l++];
            while (r < right.Length) result[spot++] = right[r++];
            return result;
        }

        static void ThreadStart(object data)
        {
            int index = (int)data;
            Array.Sort(doubles[index]);
            events[index].Set();
        }

        static void Main(string[] args)
        {
            System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
            watch.Start();
            byte[] bytes = File.ReadAllBytes(@"..\..\..\SortGuassian\Data.dat");
            doubles = new double[][] { new double[count / 4], new double[count / 4], new double[count / 4], new double[count / 4] };
            for (int i = 0; i < 4; i++)
            {
                for (int j = 0; j < count / 4; j++)
                {
                    doubles[i][j] = BitConverter.ToDouble(bytes, i * count/4 + j * 8);
                }
            }
            Thread[] threads = new Thread[4];
            for (int i = 0; i < 4; i++)
            {
                threads[i] = new Thread(ThreadStart);
                waiting[i] = events[i] = new AutoResetEvent(false);
                threads[i].Start(i);
            }
            WaitHandle.WaitAll(waiting);
            double[] left = Merge(doubles[0], doubles[1]);
            double[] right = Merge(doubles[2], doubles[3]);
            double[] result = Merge(left, right);
            watch.Stop();
            Console.WriteLine(watch.Elapsed.ToString());
            Console.ReadKey();
        }
    }
}

Monoでソリューションを実行できますか?どうすればいいですか?

Monoを使用したことがないため、F#をコンパイルして実行できるはずです。

1
パフォーマンスを改善するために4つのスレッドを使用するように更新されました。今6秒を与えます。スペアアレイを1つだけ使用し、すべてが少なくとも1回書き込まれるため、CLRによって行われる大量のメモリをゼロに初期化しない場合、これは大幅に改善される可能性があります(おそらく5秒)。

1
私のマシンで9.598秒!あなたは現在のリーダーです:)

1
母は、Monoを使用している人から離れておくように言った!

8

分布がわかっているため、直接索引付けO(N)ソートを使用できます。(それが何であるか疑問に思っているなら、52枚のカードのデッキがあり、それをソートしたいとします。52ビンを持ち、各カードをそれ自身のビンに投げます。)

5e7ダブルスがあります。5e7 doubleの結果配列Rを割り当てます。各番号xを取得して取得しi = phi(x) * 5e7ます。基本的に行いますR[i] = x。(単純なハッシュコーディングのように)衝突する可能性のある数値を移動するなど、衝突を処理する方法があります。または、Rを数倍大きくして、一意の空の値で埋めることもできます。最後に、Rの要素を掃引します。

phi単なるガウス累積分布関数です。+/-無限大のガウス分布数を0〜1の均一な分布数に変換します。計算する簡単な方法は、テーブルのルックアップと補間を使用することです。


3
注意してください:正確な分布ではなく、おおよその分布を知っています。データはガウスの法則を使用して生成されたことがわかりますが、データは有限であるため、ガウスに正確には従いません。

@static_rtti:この場合、phiの必要な近似により、データセットIMOの不規則性よりも大きな面倒が生じます。

1
@static_rtti:正確である必要はありません。データを広げるだけでほぼ均一になるので、場所によってはあまり集まりません。

5e7ダブルスがあるとします。Rの各エントリを、たとえば5e6のdoubleベクトルのベクトルにするだけではどうでしょうか。次に、適切なベクトルの各doubleをpush_backします。ベクトルを並べ替えれば完了です。これには、入力のサイズに比例した時間がかかります。
ニールG

実際、mdkessはすでにその解決策を考え出しています。
ニールG

8

別の順次ソリューションを次に示します。

#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <ctime>

typedef unsigned long long ull;

int size;
double *dbuf, *copy;
int cnt[8][1 << 16];

void sort()
{
  const int step = 10;
  const int start = 24;
  ull mask = (1ULL << step) - 1;

  ull *ibuf = (ull *) dbuf;
  for (int i = 0; i < size; i++) {
    for (int w = start, v = 0; w < 64; w += step, v++) {
      int p = (~ibuf[i] >> w) & mask;
      cnt[v][p]++;
    }
  }

  int sum[8] = { 0 };
  for (int i = 0; i <= mask; i++) {
    for (int w = start, v = 0; w < 64; w += step, v++) {
      int tmp = sum[v] + cnt[v][i];
      cnt[v][i] = sum[v];
      sum[v] = tmp;
    }
  }

  for (int w = start, v = 0; w < 64; w += step, v++) {
    ull *ibuf = (ull *) dbuf;
    for (int i = 0; i < size; i++) {
      int p = (~ibuf[i] >> w) & mask;
      copy[cnt[v][p]++] = dbuf[i];
    }

    double *tmp = copy;
    copy = dbuf;
    dbuf = tmp;
  }

  for (int p = 0; p < size; p++)
    if (dbuf[p] >= 0.) {
      std::reverse(dbuf + p, dbuf + size);
      break;
    }

  // Insertion sort
  for (int i = 1; i < size; i++) {
    double value = dbuf[i];
    if (value < dbuf[i - 1]) {
      dbuf[i] = dbuf[i - 1];
      int p = i - 1;
      for (; p > 0 && value < dbuf[p - 1]; p--)
        dbuf[p] = dbuf[p - 1];
      dbuf[p] = value;
    }
  }
}

int main(int argc, char **argv) {
  size = atoi(argv[1]);
  dbuf = new double[size];
  copy = new double[size];

  FILE *f = fopen("gaussian.dat", "r");
  fread(dbuf, size, sizeof(double), f);
  fclose(f);

  clock_t c0 = clock();
  sort();
  printf("Finished after %.3f\n", (double) ((clock() - c0)) / CLOCKS_PER_SEC);
  return 0;
}

マルチスレッドソリューションに勝るものはないでしょうが、i7ラップトップのタイミングは次のとおりです(stdsortは別の回答で提供されているC ++ソリューションです)。

$ g++ -O3 mysort.cpp -o mysort && ./mysort 50000000
Finished after 2.10
$ g++ -O3 stdsort.cpp -o stdsort && ./stdsort
Finished after 7.12

このソリューションは、時間の複雑さ線形であることに注意してください(doubleの特別な表現を使用しているため)。

編集:要素の順序が増加するように修正しました。

編集:速度をほぼ0.5秒改善しました。

編集:速度をさらに0.7秒改善しました。アルゴリズムをよりキャッシュフレンドリーにしました。

編集:速度がさらに1秒改善されました。50.000.000個の要素しかないので、仮数を部分的にソートし、挿入ソート(キャッシュフレンドリー)を使用して、アウトオブプレース要素を修正できます。このアイデアは、最後の基数ソートループから約2回の反復を削除します。

編集:0.16秒短縮。ソート順が逆になっている場合、最初のstd :: reverseは削除できます。


今、それは面白くなってきています!どのようなソートアルゴリズムですか?
-static_rtti

2
最下位桁の基数ソート。仮数、次に指数、次に記号をソートできます。ここで紹介するアルゴリズムは、このアイデアをさらに一歩進めたものです。別の回答で提供されているパーティション化のアイデアを使用して並列化できます。
アレクサンドル

シングルスレッドソリューションでは2.552秒と非常に高速です。データが正規分布しているという事実を利用するためにソリューションを変更できると思いますか?おそらく、現在の最高のマルチスレッドソリューションよりも優れている可能性があります。
-static_rtti

1
@static_rtti:Damascus Steelはすでにこの実装のマルチスレッドバージョンを公開しています。このアルゴリズムのキャッシュ動作を改善したので、より良いタイミングが得られるはずです。この新しいバージョンをテストしてください。
アレクサンドル

2
最新のテストでは1.459秒。このソリューションは私のルールでは勝者ではありませんが、大きな称賛に値します。おめでとうございます!
-static_rtti

6

Christian Ammerのソリューションを採用し、Intelのスレッドビルディングブロックと並列化する

#include <iostream>
#include <fstream>
#include <algorithm>
#include <vector>
#include <ctime>
#include <tbb/parallel_sort.h>

int main(void)
{
    std::ifstream ifs("gaussian.dat", std::ios::binary | std::ios::in);
    std::vector<double> values;
    values.reserve(50000000);
    double d;
    while (ifs.read(reinterpret_cast<char*>(&d), sizeof(double)))
    values.push_back(d);
    clock_t c0 = clock();
    tbb::parallel_sort(values.begin(), values.end());
    std::cout << "Finished after "
              << static_cast<double>((clock() - c0)) / CLOCKS_PER_SEC
              << std::endl;
}

IntelのPerformance Primitives(IPP)ライブラリにアクセスできる場合、その基数ソートを使用できます。交換するだけ

#include <tbb/parallel_sort.h>

#include "ipps.h"

そして

tbb::parallel_sort(values.begin(), values.end());

std::vector<double> copy(values.size());
ippsSortRadixAscend_64f_I(&values[0], &copy[0], values.size());

私のデュアルコアラップトップでは、タイミングは

C               16.4 s
C#              20 s
C++ std::sort   7.2 s
C++ tbb         5 s
C++ ipp         4.5 s
python          too long

1
2.958s!TBBはかなりクールで使いやすいようです!

2
TBBはとてつもなく素晴らしいです。これは、アルゴリズム作業に最適な抽象化レベルです。
drxzcl

5

分布の統計に基づいてピボット値を選択し、それによって同じサイズのパーティションを確保する並列クイックソートの実装はどうでしょうか?最初のピボットは平均(この場合はゼロ)、次のペアは25パーセンタイルと75パーセンタイル(+/- -0.67449標準偏差)などになり、各パーティションが残りのデータセットをさらに半分にしたり、あまり完璧ではありません。


それは事実上、私が私のものでしたことです。もちろん、私の記事を書き終える前に、あなたはこの投稿を入手しました。

5

非常にい(数字で終わる変数を使用できるのに配列を使用する理由)、しかし高速なコード(最初にstd :: threadsを試してみる) ()4,8 s)、g ++でコンパイル-std = c ++ 0x -O3 -march = native -pthreadデータをstdinに渡すだけです(50Mでのみ動作します)。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <thread>
using namespace std;
const size_t size=50000000;

void pivot(double* start,double * end, double middle,size_t& koniec){
    double * beg=start;
    end--;
    while (start!=end){
        if (*start>middle) swap (*start,*end--);
        else start++;
    }
    if (*end<middle) start+=1;
    koniec= start-beg;
}
void s(double * a, double* b){
    sort(a,b);
}
int main(){
    double *data=new double[size];
    FILE *f = fopen("gaussian.dat", "rb");
    fread(data,8,size,f);
    size_t end1,end2,end3,temp;
    pivot(data, data+size,0,end2);
    pivot(data, data+end2,-0.6745,end1);
    pivot(data+end2,data+size,0.6745,end3);
    end3+=end2;
    thread ts1(s,data,data+end1);
    thread ts2(s,data+end1,data+end2);
    thread ts3(s,data+end2,data+end3);
    thread ts4(s,data+end3,data+size);
    ts1.join(),ts2.join(),ts3.join(),ts4.join();
    //for (int i=0; i<size-1; i++){
    //  if (data[i]>data[i+1]) cerr<<"BLAD\n";
    //}
    fclose(f);
    //fwrite(data,8,size,stdout);
}

//gaussian.datファイルを読み取るように編集を変更しました。


上記のC ++ソリューションと同様に、gaussian.datを読み取るように変更できますか?

後で家に帰ってからやってみます。
-static_rtti

非常に素晴らしい解決策、あなたは現在のリーダーです(1.949s)!そして、ガウス分布の素晴らしい使用:)
static_rtti

4

C ++液を用いstd::sort(に関する最終的にはより高速のqsortより、STD対のqsortの性能::並べ替え

#include <iostream>
#include <fstream>
#include <algorithm>
#include <vector>
#include <ctime>

int main(void)
{
    std::ifstream ifs("C:\\Temp\\gaussian.dat", std::ios::binary | std::ios::in);
    std::vector<double> values;
    values.reserve(50000000);
    double d;
    while (ifs.read(reinterpret_cast<char*>(&d), sizeof(double)))
        values.push_back(d);
    clock_t c0 = clock();
    std::sort(values.begin(), values.end());
    std::cout << "Finished after "
              << static_cast<double>((clock() - c0)) / CLOCKS_PER_SEC
              << std::endl;
}

私のマシンには1GBしかなく、与えられたPythonコードでは、gaussian.dat25mioの倍数だけのファイルを作成できたため(メモリエラーなしで)、どれだけ時間がかかるかを信頼できません。しかし、std :: sortアルゴリズムの実行時間に非常に興味があります。


6.425s!予想どおり、C ++が輝いています:)

@static_rtti:ティムソートアルゴリズムを試しました(最初の質問でMatthieu M.から提案されました)。sort.hC ++でコンパイルするには、ファイルに変更を加える必要がありました。それは約2倍遅かったですstd::sort。コンパイラーの最適化のせいかもしれませんが、なぜかわかりませんか?
クリスチャンアンマー

4

これは、Alexandruの基数ソートとZjarekのスレッドスマートピボットの組み合わせです。コンパイルする

g++ -std=c++0x -pthread -O3 -march=native sorter_gaussian_radix.cxx -o sorter_gaussian_radix

STEPを定義することで基数サイズを変更できます(たとえば、-DSTEP = 11を追加します)。私のラップトップに最適なのは8(デフォルト)です。

デフォルトでは、問題を4つの部分に分割し、複数のスレッドで実行します。深度パラメーターをコマンドラインに渡すことで、これを変更できます。したがって、2つのコアがある場合は、次のように実行します。

sorter_gaussian_radix 50000000 1

コアが16個ある場合

sorter_gaussian_radix 50000000 4

現在、最大の深さは6(64スレッド)です。レベルを入れすぎると、コードが遅くなります。

私が試したものの1つは、Intel Performance Primitives(IPP)ライブラリの基数ソートです。Alexandruの実装は、IPPを約30%遅くして、IPPを健全に打ち負かします。そのバリエーションもここに含まれています(コメントアウト)。

#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <ctime>
#include <iostream>
#include <thread>
#include <vector>
#include <boost/cstdint.hpp>
// #include "ipps.h"

#ifndef STEP
#define STEP 8
#endif

const int step = STEP;
const int start_step=24;
const int num_steps=(64-start_step+step-1)/step;
int size;
double *dbuf, *copy;

clock_t c1, c2, c3, c4, c5;

const double distrib[]={-2.15387,
                        -1.86273,
                        -1.67594,
                        -1.53412,
                        -1.4178,
                        -1.31801,
                        -1.22986,
                        -1.15035,
                        -1.07752,
                        -1.00999,
                        -0.946782,
                        -0.887147,
                        -0.830511,
                        -0.776422,
                        -0.724514,
                        -0.67449,
                        -0.626099,
                        -0.579132,
                        -0.53341,
                        -0.488776,
                        -0.445096,
                        -0.40225,
                        -0.36013,
                        -0.318639,
                        -0.27769,
                        -0.237202,
                        -0.197099,
                        -0.157311,
                        -0.11777,
                        -0.0784124,
                        -0.0391761,
                        0,
                        0.0391761,
                        0.0784124,
                        0.11777,
                        0.157311,
                        0.197099,
                        0.237202,
                        0.27769,
                        0.318639,
                        0.36013,
                        0.40225,
                        0.445097,
                        0.488776,
                        0.53341,
                        0.579132,
                        0.626099,
                        0.67449,
                        0.724514,
                        0.776422,
                        0.830511,
                        0.887147,
                        0.946782,
                        1.00999,
                        1.07752,
                        1.15035,
                        1.22986,
                        1.31801,
                        1.4178,
                        1.53412,
                        1.67594,
                        1.86273,
                        2.15387};


class Distrib
{
  const int value;
public:
  Distrib(const double &v): value(v) {}

  bool operator()(double a)
  {
    return a<value;
  }
};


void recursive_sort(const int start, const int end,
                    const int index, const int offset,
                    const int depth, const int max_depth)
{
  if(depth<max_depth)
    {
      Distrib dist(distrib[index]);
      const int middle=std::partition(dbuf+start,dbuf+end,dist) - dbuf;

      // const int middle=
      //   std::partition(dbuf+start,dbuf+end,[&](double a)
      //                  {return a<distrib[index];})
      //   - dbuf;

      std::thread lower(recursive_sort,start,middle,index-offset,offset/2,
                        depth+1,max_depth);
      std::thread upper(recursive_sort,middle,end,index+offset,offset/2,
                        depth+1,max_depth);
      lower.join(), upper.join();
    }
  else
    {
  // ippsSortRadixAscend_64f_I(dbuf+start,copy+start,end-start);

      c1=clock();

      double *dbuf_local(dbuf), *copy_local(copy);
      boost::uint64_t mask = (1 << step) - 1;
      int cnt[num_steps][mask+1];

      boost::uint64_t *ibuf = reinterpret_cast<boost::uint64_t *> (dbuf_local);

      for(int i=0;i<num_steps;++i)
        for(uint j=0;j<mask+1;++j)
          cnt[i][j]=0;

      for (int i = start; i < end; i++)
        {
          for (int w = start_step, v = 0; w < 64; w += step, v++)
            {
              int p = (~ibuf[i] >> w) & mask;
              (cnt[v][p])++;
            }
        }

      c2=clock();

      std::vector<int> sum(num_steps,0);
      for (uint i = 0; i <= mask; i++)
        {
          for (int w = start_step, v = 0; w < 64; w += step, v++)
            {
              int tmp = sum[v] + cnt[v][i];
              cnt[v][i] = sum[v];
              sum[v] = tmp;
            }
        }

      c3=clock();

      for (int w = start_step, v = 0; w < 64; w += step, v++)
        {
          ibuf = reinterpret_cast<boost::uint64_t *>(dbuf_local);

          for (int i = start; i < end; i++)
            {
              int p = (~ibuf[i] >> w) & mask;
              copy_local[start+((cnt[v][p])++)] = dbuf_local[i];
            }
          std::swap(copy_local,dbuf_local);
        }

      // Do the last set of reversals
      for (int p = start; p < end; p++)
        if (dbuf_local[p] >= 0.)
          {
            std::reverse(dbuf_local+p, dbuf_local + end);
            break;
          }

      c4=clock();

      // Insertion sort
      for (int i = start+1; i < end; i++) {
        double value = dbuf_local[i];
        if (value < dbuf_local[i - 1]) {
          dbuf_local[i] = dbuf_local[i - 1];
          int p = i - 1;
          for (; p > 0 && value < dbuf_local[p - 1]; p--)
            dbuf_local[p] = dbuf_local[p - 1];
          dbuf_local[p] = value;
        }
      }
      c5=clock();

    }
}


int main(int argc, char **argv) {
  size = atoi(argv[1]);
  copy = new double[size];

  dbuf = new double[size];
  FILE *f = fopen("gaussian.dat", "r");
  fread(dbuf, size, sizeof(double), f);
  fclose(f);

  clock_t c0 = clock();

  const int max_depth= (argc > 2) ? atoi(argv[2]) : 2;

  // ippsSortRadixAscend_64f_I(dbuf,copy,size);

  recursive_sort(0,size,31,16,0,max_depth);

  if(num_steps%2==1)
    std::swap(dbuf,copy);

  // for (int i=0; i<size-1; i++){
  //   if (dbuf[i]>dbuf[i+1])
  //     std::cout << "BAD "
  //               << i << " "
  //               << dbuf[i] << " "
  //               << dbuf[i+1] << " "
  //               << "\n";
  // }

  std::cout << "Finished after "
            << (double) (c1 - c0) / CLOCKS_PER_SEC << " "
            << (double) (c2 - c1) / CLOCKS_PER_SEC << " "
            << (double) (c3 - c2) / CLOCKS_PER_SEC << " "
            << (double) (c4 - c3) / CLOCKS_PER_SEC << " "
            << (double) (c5 - c4) / CLOCKS_PER_SEC << " "
            << "\n";

  // delete [] dbuf;
  // delete [] copy;
  return 0;
}

編集:私はAlexandruのキャッシュの改善を実装し、それは私のマシンで約30%の時間を削った。

編集:これは再帰的なソートを実装しているので、Alexandruの16コアマシンでうまく動作するはずです。また、Alexandruの最後の改善を使用し、その逆の1つを削除します。私にとって、これは20%の改善をもたらしました。

編集:2つ以上のコアがある場合に非効率を引​​き起こすサインのバグを修正しました。

編集:ラムダを削除したので、古いバージョンのgccでコンパイルします。コメントアウトされたIPPコードのバリエーションが含まれます。また、16コアで実行するためのドキュメントを修正しました。私が知る限り、これは最速の実装です。

編集:STEPが8ではない場合のバグを修正しました。スレッドの最大数を64に増やしました。タイミング情報を追加しました。


いいね 基数の並べ替えは、非常にキャッシュにやさしいです。変更してより良い結果が得られるかどうかを確認しますstep(私のラップトップでは11が最適でした)。
アレクサンドル

バグint cnt[mask]がありますint cnt[mask + 1]。より良い結果を得るには、固定値を使用しますint cnt[1 << 16]
アレクサンドル

これらのすべての解決策を今日家に帰ってから試します。
-static_rtti

1.534s !!! リーダーがいると思う
-D

@static_rtti:これをもう一度試してもらえますか?最後に試したときよりもかなり速くなっています。私のマシンでは、他のどのソリューションよりもかなり高速です。
ダマスカス鋼

2

これは本当にあなたが何をしたいかに依存していると思います。ガウスの束を並べ替えたい場合、これは役に立ちません。しかし、ソートされたガウス分布の束が必要な場合、これは可能です。これで問題が少し見逃されたとしても、実際のソートルーチンと比較するのは面白いと思います。

何かを高速にしたい場合は、実行を少なくします。

正規分布からランダムなサンプルの束を生成してからソートする代わりに、ソートされた順序で正規分布からサンプルの束を生成できます。

ここで解決策を使用して、ソートされた順序でn個の一様乱数を生成できます。次に、正規分布の逆cdf(scipy.stats.norm.ppf)を使用して、逆変換サンプリングを介して一様乱数を正規分布からの数値に変換できます。

import scipy.stats
import random

# slightly modified from linked stackoverflow post
def n_random_numbers_increasing(n):
  """Like sorted(random() for i in range(n))),                                
  but faster because we avoid sorting."""
  v = 1.0
  while n:
    v *= random.random() ** (1.0 / n)
    yield 1 - v
    n -= 1

def n_normal_samples_increasing(n):
  return map(scipy.stats.norm.ppf, n_random_numbers_increasing(n))

手を汚したい場合は、何らかの反復法を使用し、以前の結果を最初の推測として使用することで、多くの逆cdf計算を高速化できる可能性があると思います。推測は非常に近いため、おそらく1回の反復で高い精度が得られます。


2
いい答えですが、それは不正行為です:)私の質問の考え方は、ソートアルゴリズムが非常に注目されている一方で、ソートのためのデータに関する事前知識の使用に関する文献はほとんどないということですこの問題に対処して、素晴らしい利益を報告しました。それでは何が可能か見てみましょう!

2

このMain()で変更するGuvanteのソリューションを試してみてください。1/ 4 IOの読み取りが完了するとすぐにソートが開始され、テストでは高速になります。

    static void Main(string[] args)
    {
        FileStream filestream = new FileStream(@"..\..\..\gaussian.dat", FileMode.Open, FileAccess.Read);
        doubles = new double[][] { new double[count / 4], new double[count / 4], new double[count / 4], new double[count / 4] };
        Thread[] threads = new Thread[4];

        for (int i = 0; i < 4; i++)
        {
            byte[] bytes = new byte[count * 4];
            filestream.Read(bytes, 0, count * 4);

            for (int j = 0; j < count / 4; j++)
            {
                doubles[i][j] = BitConverter.ToDouble(bytes, i * count/4 + j * 8);
            }

            threads[i] = new Thread(ThreadStart);
            waiting[i] = events[i] = new AutoResetEvent(false);
            threads[i].Start(i);    
        }

        WaitHandle.WaitAll(waiting);
        double[] left = Merge(doubles[0], doubles[1]);
        double[] right = Merge(doubles[2], doubles[3]);
        double[] result = Merge(left, right);
        Console.ReadKey();
    }
}

8.933s。やや高速:)

2

あなたは分布を知っているので、私の考えは、それぞれが同じ予想要素数を持つk個のバケットを作ることです(分布を知っているので、これを計算できます)。次に、O(n)時間で配列をスイープし、要素をバケットに入れます。

次に、バケットを同時にソートします。k個のバケットとn個の要素があるとします。バケットのソートには(n / k)lg(n / k)時間かかります。ここで、使用できるプロセッサがp個あると仮定します。バケットは個別にソートできるため、ceil(k / p)の乗数を処理する必要があります。これにより、n + ceil(k / p)*(n / k)lg(n / k)の最終ランタイムが得られます。これは、kを適切に選択した場合、n lg nよりもかなり速いはずです。


これが最善の解決策だと思います。
ニールG

バケツになる要素の数が正確にわからないため、実際には数学が間違っています。そうは言っても、これは良い答えだと思います。
プーレジャポン

@pouejapon:そのとおりです。
ニールG

この答え本当にいいですね。問題は、それは本当に速くないことです。私はこれをC99で実装し(私の回答を参照)、確かに簡単にstd::sort()破れますが、Alexandruのradixsortソリューションよりもかなり遅いです。
スベンマーナハ

2

低レベルの最適化のアイデアの1つは、2つのdoubleをSSEレジスタに収めることです。したがって、各スレッドは一度に2つのアイテムを処理します。一部のアルゴリズムでは、これが複雑になる場合があります。

もう1つやることは、配列をキャッシュフレンドリーなチャンクに並べ替えてから、結果をマージすることです。2つのレベルを使用する必要があります。たとえば、L1の場合は最初の4 KB、次にL2の場合は64 KBです。

バケットの並べ替えはキャッシュの外に出ることはなく、最終的なマージはメモリを順番にウォークするため、これは非常にキャッシュフレンドリーである必要があります。

最近の計算は、メモリアクセスよりもはるかに安価です。ただし、アイテムの数が多いため、ダムキャッシュ対応のソートが複雑さの低い非キャッシュ対応バージョンよりも遅い場合、どのアレイサイズであるかを判断するのは困難です。

ただし、Windows(VC ++)で実行するため、上記の実装は提供しません。


2

線形スキャンバケットソートの実装を次に示します。基数ソートを除き、現在のすべてのシングルスレッド実装よりも高速だと思います。cdfを十分に正確に推定している場合(Webで見つかった値の線形補間を使用している場合)、過剰なスキャンの原因となる間違いをしていない場合、実行時間は線形になります。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <ctime>

using std::fill;

const double q[] = {
  0.0,
  9.865E-10,
  2.8665150000000003E-7,
  3.167E-5,
  0.001349898,
  0.022750132,
  0.158655254,
  0.5,
  0.8413447460000001,
  0.9772498679999999,
  0.998650102,
  0.99996833,
  0.9999997133485,
  0.9999999990134999,
  1.0,
};
int main(int argc, char** argv) {
  if (argc <= 1)
    return puts("No argument!");
  unsigned count = atoi(argv[1]);
  unsigned count2 = 3 * count;

  bool *ba = new bool[count2 + 1000];
  fill(ba, ba + count2 + 1000, false);
  double *a = new double[count];
  double *c = new double[count2 + 1000];

  FILE *f = fopen("gaussian.dat", "rb");
  if (fread(a, 8, count, f) != count)
    return puts("fread failed!");
  fclose(f);

  int i;
  int j;
  bool s;
  int t;
  double z;
  double p;
  double d1;
  double d2;
  for (i = 0; i < count; i++) {
    s = a[i] < 0;
    t = a[i];
    if (s) t--;
    z = a[i] - t;
    t += 7;
    if (t < 0) {
      t = 0;
      z = 0;
    } else if (t >= 14) {
      t = 13;
      z = 1;
    }
    p = q[t] * (1 - z) + q[t + 1] * z;
    j = count2 * p;
    while (ba[j] && c[j] < a[i]) {
      j++;
    }
    if (!ba[j]) {
      ba[j] = true;
      c[j] = a[i];
    } else {
      d1 = c[j];
      c[j] = a[i];
      j++;
      while (ba[j]) {
        d2 = c[j];
        c[j] = d1;
        d1 = d2;
        j++;
      }
      c[j] = d1;
      ba[j] = true;
    }
  }
  i = 0;
  int max = count2 + 1000;
  for (j = 0; j < max; j++) {
    if (ba[j]) {
      a[i++] = c[j];
    }
  }
  // for (i = 0; i < count; i += 1) {
  //   printf("here %f\n", a[i]);
  // }
  return 0;
}

1
今日家に帰ってからこれを試してみます。それまでの間、あなたのコードは非常にいと言えますか?:-D
static_rtti

3.071s!シングルスレッドソリューションとしては悪くありません!
-static_rtti

2

以前の投稿を編集できない理由がわからないので、ここに新しいバージョンがあります。0.2秒高速ですが(CPU時間(ユーザー)で約1.5秒高速)。このソリューションには2つのプログラムがあり、最初にバケットソートの正規分布の変位値を事前計算し、テーブルt [double * scale] =バケットインデックスに格納します。スケールは、倍精度へのキャストを可能にする任意の数値です。次に、メインプログラムはこのデータを使用して、ダブルを正しいバケットに配置できます。1つの欠点があります。データがガウスではない場合、正しく動作しません(また、正規分布で正しく動作する可能性はほとんどありません)。 ::ソート())。

コンパイル:g ++ => http://pastebin.com/WG7pZEzHヘルパープログラム

g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZPメインのソートプログラム


1.621s!私はあなたがリーダーだと思いますが、これらのすべての答えで急速に道を失います:)
static_rtti

2

ここでは別のシーケンシャルなソリューションです。これは、要素が正規分布しているという事実を利用しており、この考え方は、線形時間に近いソートを得るために一般的に適用できると思います。

アルゴリズムは次のとおりです。

  • おおよそのCDF(phi()実装の機能を参照)
  • すべての要素について、ソートされた配列のおおよその位置を計算します。 size * phi(x)
  • 最終的な位置に近い新しい配列に要素を配置します
    • 私の実装では、宛先配列にいくつかのギャップがあるので、挿入するときにあまり多くの要素を移動する必要はありません。
  • insertsortを使用して、最終要素をソートします(最終位置までの距離が定数より小さい場合、insertsortは線形です)。

残念ながら、隠された定数は非常に大きく、この解決策は基数ソートアルゴリズムの2倍の速度です。


1
2.470s!とてもいいアイデア。アイデアが興味深い場合、ソリューションが最速でないことは問題ではありません:)
static_rtti

1
これは私のものと同じですが、ファイ計算をグループ化し、シフトを一緒にグループ化してキャッシュパフォーマンスを向上させますよね?
ジョンデリー

@jonderry:私はあなたの解決策を支持しました、今それが何をするのか理解しました。アイデアを盗むつもりはありませんでした。私の(非公式の)テストセットに
アレクサンドル

2

Intelのスレッドビルディングブロックを使用した私の個人的なお気に入りは既に投稿されていますが、JDK 7とその新しいfork / join APIを使用した粗雑な並列ソリューションを以下に示します。

import java.io.FileInputStream;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.*;
import static java.nio.channels.FileChannel.MapMode.READ_ONLY;
import static java.nio.ByteOrder.LITTLE_ENDIAN;


/**
 * 
 * Original Quicksort: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
 *
 */
public class ForkJoinQuicksortTask extends RecursiveAction {

    public static void main(String[] args) throws Exception {

        double[] array = new double[Integer.valueOf(args[0])];

        FileChannel fileChannel = new FileInputStream("gaussian.dat").getChannel();
        fileChannel.map(READ_ONLY, 0, fileChannel.size()).order(LITTLE_ENDIAN).asDoubleBuffer().get(array);

        ForkJoinPool mainPool = new ForkJoinPool();

        System.out.println("Starting parallel computation");

        mainPool.invoke(new ForkJoinQuicksortTask(array));        
    }

    private static final long serialVersionUID = -642903763239072866L;
    private static final int SERIAL_THRESHOLD = 0x1000;

    private final double a[];
    private final int left, right;

    public ForkJoinQuicksortTask(double[] a) {this(a, 0, a.length - 1);}

    private ForkJoinQuicksortTask(double[] a, int left, int right) {
        this.a = a;
        this.left = left;
        this.right = right;
    }

    @Override
    protected void compute() {
        if (right - left < SERIAL_THRESHOLD) {
            Arrays.sort(a, left, right + 1);
        } else {
            int pivotIndex = partition(a, left, right);
            ForkJoinTask<Void> t1 = null;

            if (left < pivotIndex)
                t1 = new ForkJoinQuicksortTask(a, left, pivotIndex).fork();
            if (pivotIndex + 1 < right)
                new ForkJoinQuicksortTask(a, pivotIndex + 1, right).invoke();

            if (t1 != null)
                t1.join();
        }
    }

    public static int partition(double[] a, int left, int right) {
        // chose middle value of range for our pivot
        double pivotValue = a[left + (right - left) / 2];

        --left;
        ++right;

        while (true) {
            do
                ++left;
            while (a[left] < pivotValue);

            do
                --right;
            while (a[right] > pivotValue);

            if (left < right) {
                double tmp = a[left];
                a[left] = a[right];
                a[right] = tmp;
            } else {
                return right;
            }
        }
    }    
}

重要な免責事項:fork / joinのクイックソート適応をhttps://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallelから取得しました

これを実行するには、JDK 7のベータビルド(http://jdk7.java.net/download.html)が必要です。

2.93Ghz Quad Core i7(OS X)の場合:

Pythonリファレンス

time python sort.py 50000000
sorting...

real    1m13.885s
user    1m11.942s
sys     0m1.935s

Java JDK 7 fork / join

time java ForkJoinQuicksortTask 50000000
Starting parallel computation

real    0m2.404s
user    0m10.195s
sys     0m0.347s

また、並列読み取りとバイトをダブルに変換するいくつかの実験を試みましたが、違いは見られませんでした。

更新:

誰かがデータの並列ロードを試してみたい場合は、並列ロードのバージョンを以下に示します。理論的には、IOデバイスに十分な並列容量がある場合は、これによりまだ少し速くなる可能性があります(通常はSSDが行います)。バイトからDoubleを作成する際にもオーバーヘッドが発生するため、並行して高速化される可能性もあります。私のシステム(Ubuntu 10.10 / Nehalem Quad / Intel X25M SSD、およびOS X 10.6 / i7 Quad / Samsung SSD)では、実際の違いは見られませんでした。

import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.nio.channels.FileChannel.MapMode.READ_ONLY;

import java.io.FileInputStream;
import java.nio.DoubleBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveAction;


/**
 *
 * Original Quicksort: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
 *
 */
public class ForkJoinQuicksortTask extends RecursiveAction {

   public static void main(String[] args) throws Exception {

       ForkJoinPool mainPool = new ForkJoinPool();

       double[] array = new double[Integer.valueOf(args[0])];
       FileChannel fileChannel = new FileInputStream("gaussian.dat").getChannel();
       DoubleBuffer buffer = fileChannel.map(READ_ONLY, 0, fileChannel.size()).order(LITTLE_ENDIAN).asDoubleBuffer();

       mainPool.invoke(new ReadAction(buffer, array, 0, array.length));
       mainPool.invoke(new ForkJoinQuicksortTask(array));
   }

   private static final long serialVersionUID = -642903763239072866L;
   private static final int SERIAL_THRESHOLD = 0x1000;

   private final double a[];
   private final int left, right;

   public ForkJoinQuicksortTask(double[] a) {this(a, 0, a.length - 1);}

   private ForkJoinQuicksortTask(double[] a, int left, int right) {
       this.a = a;
       this.left = left;
       this.right = right;
   }

   @Override
   protected void compute() {
       if (right - left < SERIAL_THRESHOLD) {
           Arrays.sort(a, left, right + 1);
       } else {
           int pivotIndex = partition(a, left, right);
           ForkJoinTask<Void> t1 = null;

           if (left < pivotIndex)
               t1 = new ForkJoinQuicksortTask(a, left, pivotIndex).fork();
           if (pivotIndex + 1 < right)
               new ForkJoinQuicksortTask(a, pivotIndex + 1, right).invoke();

           if (t1 != null)
               t1.join();
       }
   }

   public static int partition(double[] a, int left, int right) {
       // chose middle value of range for our pivot
       double pivotValue = a[left + (right - left) / 2];

       --left;
       ++right;

       while (true) {
           do
               ++left;
           while (a[left] < pivotValue);

           do
               --right;
           while (a[right] > pivotValue);

           if (left < right) {
               double tmp = a[left];
               a[left] = a[right];
               a[right] = tmp;
           } else {
               return right;
           }
       }
   }

}

class ReadAction extends RecursiveAction {

   private static final long serialVersionUID = -3498527500076085483L;

   private final DoubleBuffer buffer;
   private final double[] array;
   private final int low, high;

   public ReadAction(DoubleBuffer buffer, double[] array, int low, int high) {
       this.buffer = buffer;
       this.array = array;
       this.low = low;
       this.high = high;
   }

   @Override
   protected void compute() {
       if (high - low < 100000) {
           buffer.position(low);
           buffer.get(array, low, high-low);
       } else {
           int middle = (low + high) >>> 1;

           invokeAll(new ReadAction(buffer.slice(), array, low, middle),  new ReadAction(buffer.slice(), array, middle, high));
       }
   }
}

Update2:

12コアの開発マシンの1つでコードを実行し、わずかな修正を加えて、一定量のコアを設定しました。これにより、次の結果が得られました。

Cores  Time
1      7.568s
2      3.903s
3      3.325s
4      2.388s
5      2.227s
6      1.956s
7      1.856s
8      1.827s
9      1.682s
10     1.698s
11     1.620s
12     1.503s

このシステムでは、1m2.994sのPythonバージョンと1.925sのZjarekのC ++バージョンも試しました(何らかの理由で、ZjarekのC ++バージョンはstatic_rttiのコンピューターで比較的高速に実行されるようです)。

また、ファイルサイズを100,000,000倍に倍増するとどうなるかを試しました。

Cores  Time
1      15.056s
2      8.116s
3      5.925s
4      4.802s
5      4.430s
6      3.733s
7      3.540s
8      3.228s
9      3.103s
10     2.827s
11     2.784s
12     2.689s

この場合、ZjarekのC ++バージョンは3.968秒かかりました。Pythonはここで時間がかかりすぎました。

150,000,000ダブル:

Cores  Time
1      23.295s
2      12.391s
3      8.944s
4      6.990s
5      6.216s
6      6.211s
7      5.446s
8      5.155s
9      4.840s
10     4.435s
11     4.248s
12     4.174s

この場合、ZjarekのC ++バージョンは6.044秒でした。Pythonを試してさえいませんでした。

C ++バージョンは、Javaが少し変動する結果と非常に一貫しています。最初に、問題が大きくなると効率が少し上がりますが、再び効率が下がります。


1
このコードは、私にとってdouble値を正しく解析しません。ファイルの値を正しく解析するためにJava 7は必要ですか?
ジョンデリー

1
ああ、愚かな私。IOコードを複数行から1行にローカルでリファクタリングした後、エンディアンを再度設定するのを忘れました。もちろん、fork / joinをJava 6に個別に追加しない限り、通常Java 7が必要になります。
アルジャン

私のマシンでは3.411秒。悪くはないが、koumes21のJavaソリューションより遅い:)
static_rtti

1
ここでもローカルでkoumes21のソリューションを試して、システムの相対的な違いを確認します。とにかく、それははるかに賢い解決策であるため、koumes21から「失う」ことに恥はありません。これは、フォーク/ジョインプールに投入されるほぼ標準のクイックソートです;)
アルジャン

1

従来のpthreadを使用するバージョン。Guvanteの回答からコピーしたマージのコード。でコンパイルしg++ -O3 -pthreadます。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <algorithm>

static unsigned int nthreads = 4;
static unsigned int size = 50000000;

typedef struct {
  double *array;
  int size;
} array_t;


void 
merge(double *left, int leftsize,
      double *right, int rightsize,
      double *result)
{
  int l = 0, r = 0, insertat = 0;
  while (l < leftsize && r < rightsize) {
    if (left[l] < right[r])
      result[insertat++] = left[l++];
    else
      result[insertat++] = right[r++];
  }

  while (l < leftsize) result[insertat++] = left[l++];
  while (r < rightsize) result[insertat++] = right[r++];
}


void *
run_thread(void *input)
{
  array_t numbers = *(array_t *)input;
  std::sort(numbers.array, numbers.array+numbers.size); 
  pthread_exit(NULL);
}

int 
main(int argc, char **argv) 
{
  double *numbers = (double *) malloc(size * sizeof(double));

  FILE *f = fopen("gaussian.dat", "rb");
  if (fread(numbers, sizeof(double), size, f) != size)
    return printf("Reading gaussian.dat failed");
  fclose(f);

  array_t worksets[nthreads];
  int worksetsize = size / nthreads;
  for (int i = 0; i < nthreads; i++) {
    worksets[i].array=numbers+(i*worksetsize);
    worksets[i].size=worksetsize;
  }

  pthread_attr_t attributes;
  pthread_attr_init(&attributes);
  pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_JOINABLE);

  pthread_t threads[nthreads];
  for (int i = 0; i < nthreads; i++) {
    pthread_create(&threads[i], &attributes, &run_thread, &worksets[i]);
  }

  for (int i = 0; i < nthreads; i++) {
    pthread_join(threads[i], NULL);
  }

  double *tmp = (double *) malloc(size * sizeof(double));
  merge(numbers, worksetsize, numbers+worksetsize, worksetsize, tmp);
  merge(numbers+(worksetsize*2), worksetsize, numbers+(worksetsize*3), worksetsize, tmp+(size/2));
  merge(tmp, worksetsize*2, tmp+(size/2), worksetsize*2, numbers);

  /*
  printf("Verifying result..\n");
  for (int i = 0; i < size - 1; i++) {
    if (numbers[i] > numbers[i+1])
      printf("Result is not correct\n");
  }
  */

  pthread_attr_destroy(&attributes);
  return 0;
}  

私のラップトップでは、次の結果が得られます。

real    0m6.660s
user    0m9.449s
sys     0m1.160s

1

以下は、既知のディストリビューションを実際に利用しようとするシーケンシャルC99実装です。基本的に、分布情報を使用してバケットソートの1ラウンドを行い、次にバケットの制限内で均一な分布を想定し、最後にデータをコピーして元のバッファーにコピーするために変更された選択ソートを想定して、各バケットでクイックソートを数ラウンド行います。クイックソートは分割ポイントを記憶するため、選択ソートは小さな塊でのみ操作する必要があります。そして、その複雑さにもかかわらず(という理由で)、それは本当に速くさえありません。

Φの評価を高速にするために、値はいくつかのポイントでサンプリングされ、後で線形補間のみが使用されます。近似が厳密に単調である限り、Φが正確に評価されるかどうかは実際には関係ありません。

ビンのサイズは、ビンのオーバーフローの可能性が無視できるように選択されます。より正確には、現在のパラメーターでは、50000000要素のデータセットがビンオーバーフローを引き起こす可能性は3.65e-09です。(これは、ポアソン分布の生存関数を使用して計算できます。)

コンパイルするには、使用してください

gcc -std=c99 -msse3 -O3 -ffinite-math-only

他のソリューションよりもかなり多くの計算が行われるため、少なくとも合理的に高速にするためにこれらのコンパイラフラグが必要です。なしで-msse3から変換doubleすることがint非常に遅くなります。アーキテクチャがSSE3をサポートしていない場合、これらの変換はlrint()関数を使用して実行することもできます。

コードはかなりいです-これが「合理的に読みやすい」という要件を満たしているかどうかはわかりません...

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

#define N 50000000
#define BINSIZE 720
#define MAXBINSIZE 880
#define BINCOUNT (N / BINSIZE)
#define SPLITS 64
#define PHI_VALS 513

double phi_vals[PHI_VALS];

int bin_index(double x)
{
    double y = (x + 8.0) * ((PHI_VALS - 1) / 16.0);
    int interval = y;
    y -= interval;
    return (1.0 - y) * phi_vals[interval] + y * phi_vals[interval + 1];
}

double bin_value(int bin)
{
    int left = 0;
    int right = PHI_VALS - 1;
    do
    {
        int centre = (left + right) / 2;
        if (bin < phi_vals[centre])
            right = centre;
        else
            left = centre;
    } while (right - left > 1);
    double frac = (bin - phi_vals[left]) / (phi_vals[right] - phi_vals[left]);
    return (left + frac) * (16.0 / (PHI_VALS - 1)) - 8.0;
}

void gaussian_sort(double *restrict a)
{
    double *b = malloc(BINCOUNT * MAXBINSIZE * sizeof(double));
    double **pos = malloc(BINCOUNT * sizeof(double*));
    for (size_t i = 0; i < BINCOUNT; ++i)
        pos[i] = b + MAXBINSIZE * i;
    for (size_t i = 0; i < N; ++i)
        *pos[bin_index(a[i])]++ = a[i];
    double left_val, right_val = bin_value(0);
    for (size_t bin = 0, i = 0; bin < BINCOUNT; ++bin)
    {
        left_val = right_val;
        right_val = bin_value(bin + 1);
        double *splits[SPLITS + 1];
        splits[0] = b + bin * MAXBINSIZE;
        splits[SPLITS] = pos[bin];
        for (int step = SPLITS; step > 1; step >>= 1)
            for (int left_split = 0; left_split < SPLITS; left_split += step)
            {
                double *left = splits[left_split];
                double *right = splits[left_split + step] - 1;
                double frac = (double)(left_split + (step >> 1)) / SPLITS;
                double pivot = (1.0 - frac) * left_val + frac * right_val;
                while (1)
                {
                    while (*left < pivot && left <= right)
                        ++left;
                    while (*right >= pivot && left < right)
                        --right;
                    if (left >= right)
                        break;
                    double tmp = *left;
                    *left = *right;
                    *right = tmp;
                    ++left;
                    --right;
                }
                splits[left_split + (step >> 1)] = left;
            }
        for (int left_split = 0; left_split < SPLITS; ++left_split)
        {
            double *left = splits[left_split];
            double *right = splits[left_split + 1] - 1;
            while (left <= right)
            {
                double *min = left;
                for (double *tmp = left + 1; tmp <= right; ++tmp)
                    if (*tmp < *min)
                        min = tmp;
                a[i++] = *min;
                *min = *right--;
            }
        }
    }
    free(b);
    free(pos);
}

int main()
{
    double *a = malloc(N * sizeof(double));
    FILE *f = fopen("gaussian.dat", "rb");
    assert(fread(a, sizeof(double), N, f) == N);
    fclose(f);
    for (int i = 0; i < PHI_VALS; ++i)
    {
        double x = (i * (16.0 / PHI_VALS) - 8.0) / sqrt(2.0);
        phi_vals[i] =  (erf(x) + 1.0) * 0.5 * BINCOUNT;
    }
    gaussian_sort(a);
    free(a);
}

4.098s!-lmを追加してコンパイルする必要がありました(erf用)。
-static_rtti

1
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <memory.h>
#include <algorithm>

// maps [-inf,+inf] to (0,1)
double normcdf(double x) {
        return 0.5 * (1 + erf(x * M_SQRT1_2));
}

int calcbin(double x, int bins) {
        return (int)floor(normcdf(x) * bins);
}

int *docensus(int bins, int n, double *arr) {
        int *hist = calloc(bins, sizeof(int));
        int i;
        for(i = 0; i < n; i++) {
                hist[calcbin(arr[i], bins)]++;
        }
        return hist;
}

void partition(int bins, int *orig_counts, double *arr) {
        int *counts = malloc(bins * sizeof(int));
        memcpy(counts, orig_counts, bins*sizeof(int));
        int *starts = malloc(bins * sizeof(int));
        int b, i;
        starts[0] = 0;
        for(i = 1; i < bins; i++) {
                starts[i] = starts[i-1] + counts[i-1];
        }
        for(b = 0; b < bins; b++) {
                while (counts[b] > 0) {
                        double v = arr[starts[b]];
                        int correctbin;
                        do {
                                correctbin = calcbin(v, bins);
                                int swappos = starts[correctbin];
                                double tmp = arr[swappos];
                                arr[swappos] = v;
                                v = tmp;
                                starts[correctbin]++;
                                counts[correctbin]--;
                        } while (correctbin != b);
                }
        }
        free(counts);
        free(starts);
}


void sortbins(int bins, int *counts, double *arr) {
        int start = 0;
        int b;
        for(b = 0; b < bins; b++) {
                std::sort(arr + start, arr + start + counts[b]);
                start += counts[b];
        }
}


void checksorted(double *arr, int n) {
        int i;
        for(i = 1; i < n; i++) {
                if (arr[i-1] > arr[i]) {
                        printf("out of order at %d: %lf %lf\n", i, arr[i-1], arr[i]);
                        exit(1);
                }
        }
}


int main(int argc, char *argv[]) {
        if (argc == 1 || argv[1] == NULL) {
                printf("Expected data size as argument\n");
                exit(1);
        }
        int n = atoi(argv[1]);
        const int cachesize = 128 * 1024; // a guess
        int bins = (int) (1.1 * n * sizeof(double) / cachesize);
        if (argc > 2) {
                bins = atoi(argv[2]);
        }
        printf("Using %d bins\n", bins);
        FILE *f = fopen("gaussian.dat", "rb");
        if (f == NULL) {
                printf("Couldn't open gaussian.dat\n");
                exit(1);
        }
        double *arr = malloc(n * sizeof(double));
        fread(arr, sizeof(double), n, f);
        fclose(f);

        int *counts = docensus(bins, n, arr);
        partition(bins, counts, arr);
        sortbins(bins, counts, arr);
        checksorted(arr, n);

        return 0;
}

これは、erf()を使用して各要素をビンに適切に配置し、各ビンをソートします。配列を完全にその場に保持します。

最初のパス:docensus()は、各ビン内の要素の数をカウントします。

2番目のパス:partition()は配列を置換し、各要素を適切なビンに配置します

3番目のパス:sortbins()は各ビンでqsortを実行します。

それは一種の素朴で、すべての値に対して高価なerf()関数を2回呼び出します。最初と3番目のパスは潜在的に並列化可能です。2つ目は高度にシリアルであり、おそらく非常にランダムなメモリアクセスパターンによって速度が低下します。また、CPUのメモリ速度に対する比率に応じて、各doubleのビン番号をキャッシュする価値があります。

このプログラムでは、使用するビンの数を選択できます。コマンドラインに2つ目の数字を追加するだけです。私はgcc -O3でコンパイルしましたが、私のマシンは非常に弱く、良いパフォーマンスの数値を伝えることができません。

編集:プーフ!私のCプログラムは、std :: sortを使用して魔法のようにC ++プログラムに変換されました!


phiを使用すると、stdnormal_cdfを高速化できます。
アレクサンドル

おおよそ何個のビンを入れる必要がありますか?
-static_rtti

@Alexandru:normcdfに区分的線形近似を追加しましたが、約5%の速度しか得られませんでした。
欲求不満

@static_rtti:置く必要はありません。デフォルトでは、コードはビンの数を選択するため、ビンの平均サイズは128kbの10/11です。ビンが少なすぎると、パーティション分割の利点が得られません。キャッシュがオーバーフローするため、多すぎるとパーティションフェーズが機能しなくなります。
欲求不満

10.6s!ビンの数を少し試してみましたが、5000で最高の結果が得られました(デフォルト値の3356をわずかに上回りました)。私はあなたのソリューションのパフォーマンスがはるかに良くなると期待されていたと言わなければなりません...たぶん、あなたは潜在的に高速なC ++ソリューションのstd :: sortの代わりにqsortを使用しているという事実ですか?
-static_rtti

1

Michael Herf(Radix Tricks)による基数ソートの実装をご覧ください。私のマシンstd::sortでは、最初の答えのアルゴリズムに比べてソートが5倍高速でした。ソート関数の名前はRadixSort11です。

int main(void)
{
    std::ifstream ifs("C:\\Temp\\gaussian.dat", std::ios::binary | std::ios::in);
    std::vector<float> v;
    v.reserve(50000000);
    double d;
    while (ifs.read(reinterpret_cast<char*>(&d), sizeof(double)))
        v.push_back(static_cast<float>(d));
    std::vector<float> vres(v.size(), 0.0);
    clock_t c0 = clock();
    RadixSort11(&v[0], &vres[0], v.size());
    std::cout << "Finished after: "
              << static_cast<double>(clock() - c0) / CLOCKS_PER_SEC << std::endl;
    return 0;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.