竞赛:排序大量高斯分布数据的最快方法


71

跟随对此问题的兴趣,我认为通过提出比赛来使答案更加客观和量化会很有趣。

这个想法很简单:我生成了一个二进制文件,其中包含5000万个高斯分布的双精度(平均:0,stdev 1)。目标是制作一个程序,以便尽可能快地对它们进行排序。python中一个非常简单的参考实现需要1m4s的时间来完成。我们能走多低?

规则如下:用一个程序打开文件“ gaussian.dat”并对内存中的数字进行排序(无需输出),并提供有关构建和运行该程序的说明。该程序必须能够在我的Arch Linux机器上运行(这意味着您可以使用可以在此系统上轻松安装的任何编程语言或库)。

该程序必须具有合理的可读性,以便我可以确保它可以安全启动(请不要使用仅汇编程序的解决方案!)。

我将在我的机器(四核,4 GB 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,对不起

编辑#2:请注意,比赛的重点是看我们是否可以使用有关数据的先验信息。它不应该是不同编程语言实现之间的小麻烦!


1
取每个值并将其直接移动到其“预期”位置,重复该替换值。不知道如何解决这个问题。完成后,对气泡进行排序,直到完成为止(应该经过两次)。

1
如果

1
@static_rtti-作为重度CG用户,这正是“我们”喜欢在CG.SE上尝试的事情。对于任何阅读模块,请将其移至CG,不要关闭它。
arrdem 2011年

1
欢迎来到CodeGolf.SE!我已经从SO原始文档中清除了很多有关该元素属于或不属于何处的评论,并重新标记以更接近CodeGolf.SE主流。
dmckee 2011年

2
这里一个棘手的问题是,我们寻找客观的获胜标准,并且“最快”引入了平台依赖性...在cpython虚拟机上实现的O(n ^ {1.2})算法击败了O(n ^ {1.3} )在c中实现具有类似常数的算法?我通常建议对每种解决方案的性能特征进行一些讨论,因为这可以帮助人们判断发生了什么。
dmckee 2011年

Answers:


13

这是C ++中的解决方案,该解决方案首先将数字划分为具有相同预期元素数量的存储桶,然后分别对每个存储桶进行排序。它根据Wikipedia的一些公式预先计算累积分布函数表,然后对该表中的值进行插值以获得快速近似值。

在多个线程中运行几个步骤,以利用四个核心。

#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,则与该存储桶数量相比,该程序花费的运行时间更长。

编辑:相同的算法,不同的编程语言。我使用C ++代替Java,并且计算机上的运行时间从〜3.2s减少到〜2.35s。最佳存储桶数仍然约为256(同样在我的计算机上)。

顺便说一句,tbb确实很棒。

编辑:我受到Alexandru出色解决方案的启发,并在最后阶段用其基数排序的修改版本替换了std :: sort。我确实使用了另一种方法来处理正数/负数,即使它需要更多遍历数组。我还决定对数组进行完全排序,并删除插入排序。稍后,我将花一些时间测试这些更改如何影响性能并可能恢复它们。但是,通过使用基数排序,时间从〜2.35s减少到〜1.63s。


真好 我的分数是3.055。我能达到的最低值是6.3。我正在筛选您的数据,以使统计数据更好。您为什么选择256作为存储桶数?我尝试了128和512,但256最好。
Scott

为什么我选择256作为存储桶数?我尝试了128和512,但256最好。:)我凭经验发现它,我不确定为什么增加存储桶的数量会使算法变慢-内存分配不应该花那么长时间。也许与缓存大小有关?
k21

我的机器上为2.725s。考虑到JVM的加载时间,对于Java解决方案来说非常不错。
static_rtti 2011年

2
根据我和Arjan的解决方案(使用他的语法,因为它比我的还干净),我将您的代码切换为使用nio包,并且能够使其快0.3秒。我有一个ssd,我想知道如果没有的话可能会有什么影响。它还摆脱了您的一些琐事。修改的部分在这里。
斯科特(Scott)

3
这是我测试中最快的并行解决方案(16核CPU)。距离第二名的1.94秒只有1.22秒。
Alexandru

13

如果不聪明,只是为了提供更快的幼稚排序器,这是C语言中的一种,应该与Python相当:

#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花了不到一分钟的时间:大约11秒,而87秒。


1
在我的计算机上花费了10.086s,这使您成为当前的领导者!但我很确定我们可以做得更好:)

1
您是否可以尝试删除第二个三元运算符,并在这种情况下简单地返回1,因为在这些数据量中,随机双精度并不等于彼此相等。
Codism 2011年

@Codism:我要补充一点,我们不在乎交换等效数据的位置,因此即使获得等效值也将是一个适当的简化。

10

我根据标准偏差将其分为几部分,最好将其分解为四分之一。编辑:根据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 gig的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.185秒!我猜这是一个理想的Scala解决方案...另外,勇敢地提供了第一个实际上以某种方式使用高斯分布的解决方案!

1
我只是想与c#解决方案竞争。没想到我会击败c / c ++。另外,它对您和我的行为有很大不同。我最终使用的是openJDK,它的速度要慢得多。我不知道添加更多分区是否会对您的环境有所帮助。
Scott

9

只需将其放入一个cs文件中,然后在理论上与csc一起编译即可:(需要mono)

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
更新为使用四个线程来提高性能。现在给我6秒。请注意,如果仅使用一个备用阵列,并且避免将一吨内存初始化为零,这可以得到显着改善(可能为5秒),这由CLR完成,因为所有内容至少被写入一次。

1
我机器上的9.598s!您是当前的领导者:)

1
我妈妈告诉我不要和Mono在一起!

8

由于您知道分布是什么,因此可以使用直接索引O(N)排序。(如果您想知道这是什么,假设您有一副52张纸牌,并且想要对其进行排序。只需有52个纸箱并将每张纸牌扔进自己的纸箱中。)

您有5e7双打。分配结果数组R为5e7的两倍。取每个数字x并得到i = phi(x) * 5e7。基本上可以做到R[i] = x。有一种方法来处理冲突,例如移动它可能与之碰撞的数字(如在简单的哈希编码中)。或者,您可以将R增大几倍,并用唯一的值填充。最后,您只需清扫R的元素。

phi只是高斯累积分布函数。它将+/-无穷大之间的高斯分布数转换为0到1之间的均匀分布数。一种简单的计算方法是使用表查找和内插法。


3
注意:您知道近似分布,而不是确切分布。您知道数据是使用高斯定律生成的,但是由于它是有限的,因此并不完全遵循高斯定律。

@static_rtti:在这种情况下,phi的必要近似值会比数据集IMO中的任何不规则性产生更大的麻烦。

1
@static_rtti:不一定是准确的。它只需要散布数据就可以使数据大致均匀,因此在某些地方不会造成太多麻烦。

假设您有5e7双打。为什么不只是将R中的每个条目都设为例如double的5e6个向量。然后,将每个double推入其适当的向量中。对向量进行排序,您就完成了。输入的大小应该花费线性时间。
尼尔·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.7秒。使算法更易于缓存。

编辑:速度提高了另外1秒。由于只有50.000.000个元素,因此我可以对尾数进行部分排序,并使用insert sort(对缓存友好)来修复不适当的元素。这个想法从最后一个基数排序循环中删除了大约两次迭代。

编辑:减少0.16秒。如果排序顺序相反,则可以消除第一个std :: reverse。


现在变得有趣了!这是一种什么样的排序算法?
static_rtti 2011年

2
最小有效数字基数排序。您可以对尾数进行排序,然后对指数进行排序,然后对符号进行排序。这里介绍的算法使这一想法更进一步。可以使用其他答案中提供的分区思想来并行化它。
Alexandru

对于单线程解决方案,速度非常快:2.552秒!您认为您可以更改解决方案以利用数据呈正态分布这一事实吗?您可能会比目前最好的多线程解决方案做得更好。
static_rtti 2011年

1
@static_rtti:我看到Damascus Steel已经发布了该实现的多线程版本。我改进了该算法的缓存行为,因此您现在应该获得更好的计时。请测试此新版本。
Alexandru

2
我最新的测试中为1.459s。虽然按照我的规则,这种解决方案不是赢家,但它确实值得大赞。恭喜你!
static_rtti 2011年

6

采用Christian Ammer的解决方案并将其与英特尔的线程构建基块并行化

#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;
}

如果您有权访问英特尔的性能基元(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.958秒!TBB看起来很酷,易于使用!

2
TBB真是太棒了。这正是算法工作的正确抽象层次。
drxzcl 2011年

5

如何实现并行快速排序的实现,该实现基于分布的统计信息选择其枢轴值,从而确保分区大小相等?第一个枢轴是平均值(在这种情况下为零),第二对是第25个和第75个百分位数(+/- -0.67449标准差),依此类推,每个分区将剩余数据集减半或更多不太完美。


这实际上是我在我的工作上做的..当然,您在完成撰写之前就收到了这篇文章。

5

非常难看(为什么我可以使用以数字结尾的变量时为什么要使用数组),但是代码却很快速(我第一次尝试使用std :: threads),所以我的系统上的整个时间(实时)为1.8 s(与std :: sort相比) ()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 2011年

非常好的解决方案,您是当前的领导者(1.949秒)!很好地使用了高斯分布:)
static_rtti 2011年

4

一个C ++解决方案,使用std::sort(最终比qsort快,关于qsort与std :: sort的性能

#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.dat只有25mio double 的文件(而不会出现Memory错误)。但是我非常感兴趣std :: sort算法运行多长时间。


6.425秒!不出所料,C ++

@static_rtti:我尝试了swensons Timsort算法(正如Matthieu M.在您的第一个问题中所建议的那样)。我必须对sort.h文件进行一些更改才能使用C ++进行编译。它慢了大约两倍std::sort。不知道为什么,也许是由于编译器优化?
Christian Ammer

4

这是Alexandru的基数排序与Zjarek的线程化智能透视图的混合。用它编译

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

您可以通过定义STEP来更改基数大小(例如,添加-DSTEP = 11)。我发现最好的笔记本电脑是8(默认值)。

默认情况下,它将问题分成4部分并在多个线程上运行。您可以通过将depth参数传递给命令行来更改它。因此,如果您有两个核心,请以

sorter_gaussian_radix 50000000 1

如果您有16个核心

sorter_gaussian_radix 50000000 4

现在的最大深度为6(64个线程)。如果放置了过多的关卡,您只会降低代码速度。

我还尝试过的一件事是Intel Performance Primitives(IPP)库中的基数排序。亚历山德鲁的实施方法大大超越了IPP,IPP的速度要慢30%。这种变化也包括在这里(注释掉)。

#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的最新改进,并删除了其中一项相反的改进。对我来说,这提高了20%。

编辑:修复了一个符号错误,当存在两个以上内核时,该错误会导致效率低下。

编辑:删除了lambda,因此它将与旧版本的gcc一起编译。它包括注释掉的IPP代码变体。我还修复了在16个内核上运行的文档。据我所知,这是最快的实现。

编辑:修复了STEP不在8时的错误。将最大线程数增加到64。添加了一些计时信息。


真好 基数排序非常不友好。看看是否可以通过更改获得更好的结果step(11在我的笔记本电脑上最佳)。
Alexandru

您有一个错误:int cnt[mask]应该是int cnt[mask + 1]。为了获得更好的结果,请使用固定值int cnt[1 << 16]
Alexandru

今天晚些时候,我将尝试所有这些解决方案。
static_rtti 2011年

1.534秒!我认为我们有一个领导者:-D
static_rtti 2011年

@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逆计算。由于猜测将非常接近,因此一次迭代可能会给您带来很大的准确性。


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.933秒。稍微快一点:)

2

由于您知道分布,因此我的想法是制作k个存储桶,每个存储桶都具有相同的预期元素数量(因为您知道分布,因此可以计算出这个值)。然后在O(n)的时间内扫描阵列并将元素放入其存储桶中。

然后同时对水桶进行分类。假设您有k个桶和n个元素。桶将需要(n / k)lg(n / k)的时间进行排序。现在假设您有可以使用的p个处理器。由于存储桶可以独立排序,因此您需要处理ceil(k / p)的乘数。这样最终运行时间为n + ceil(k / p)*(n / k)lg(n / k),如果选择得当,则比n lg n快很多。


我认为这是最好的解决方案。
尼尔·G

您并不完全知道将在存储桶中存储的元素数量,因此数学实际上是错误的。话虽如此,我认为这是一个很好的答案。
poulejapon 2011年

@pouejapon:你是对的。
尼尔·G

这个答案听起来真的很好。问题是-速度不是很快。我在C99中实现了这一点(请参阅我的答案),它当然很容易击败std::sort(),但是它比Alexandru的radixsort解决方案要慢得多。
Sven Marnach 2011年

2

一种低层的优化思想是在SSE寄存器中放入两个双精度数,因此每个线程一次可以处理两个项。对于某些算法,这可能很复杂。

另一件事是对数组进行缓存友好的块排序,然后合并结果。应该使用两个级别:例如对于L1首先是4 KB,然后对于L2是64 KB。

这应该对缓存非常友好,因为存储桶排序不会超出缓存范围,并且最终合并将按顺序遍历内存。

这些天的计算比内存访问便宜得多。但是,我们有很多项,因此当哑缓存感知排序比低复杂度非缓存感知版本慢时,很难分辨出数组的大小。

但是由于我将在Windows(VC ++)中实现上述功能,因此我将不提供上述实现。


2

这是线性扫描存储桶排序实现。我认为它比除基数排序外的所有当前单线程实现都快。如果我足够准确地估计cdf(我使用的是在网上找到的值的线性插值)并且没有犯任何会导致过度扫描的错误,则它应该具有线性预期运行时间:

#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 2011年

3.071秒!对于单线程解决方案来说还不错!
static_rtti 2011年

2

我不知道,为什么我不能编辑我以前的文章,所以这里是新版本,快了0.2秒(但CPU时间(用户)快了1.5秒)。该解决方案有2个程序,首先为桶分类的正态分布预先计算分位数,然后将其存储在表中,t [double * scale] =桶索引,其中scale是任意数字,使转换成倍数成为可能。然后主程序可以使用此数据将双打放入正确的存储桶中。它有一个缺点,如果数据不是高斯数据,将无法正常工作(对于正态分布,正常工作的几率几乎为零),但是对特殊情况的修改既简单又快速(仅进行存储桶检查并降至标准数量) ::分类())。

编译:g ++ => http://pastebin.com/WG7pZEzH帮助程序

g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZP主排序程序


1.621秒!我认为您是领导者,但对于所有这些答案,我都迅速失去了方向:)
static_rtti 2011年

2

是另一个顺序解决方案。这利用了元素是正态分布这一事实,并且我认为这种想法通常适用于接近线性时间的排序。

算法是这样的:

  • 大约CDF(请参阅phi()实现中的功能)
  • 对于所有元素,请计算已排序数组中的近似位置: size * phi(x)
  • 将元素放置在靠近最终位置的新数组中
    • 在我的实现中,目标数组中有一些空白,因此插入时不必移动太多元素。
  • 使用insertsort对最终元素进行排序(如果到最终位置的距离小于常数,则insertsort是线性的)。

不幸的是,隐藏常数非常大,该解决方案的速度是基数排序算法的两倍。


1
2.470秒!很好的主意。没关系,如果想法很有趣,解决方案就不是最快的了:)
static_rtti 2011年

1
这与我的相同,但是将phi计算分组在一起,并将移位组合在一起以提高缓存性能,对吗?
jonderry 2011年

@jonderry:我了解您的解决方案后,我投票支持您的解决方案。并不是要窃取您的想法。我将您的实现包含在我的(非正式)测试集中
Alexandru

2

我个人最喜欢使用英特尔的线程构建模块,但是这里是使用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的beta版(http://jdk7.java.net/download.html)。

在我的2.93Ghz四核i7(OS X)上:

Python参考

time python sort.py 50000000
sorting...

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

Java JDK 7分叉/联接

time java ForkJoinQuicksortTask 50000000
Starting parallel computation

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

我还尝试了一些并行读取并将字节转换为双精度的实验,但是我没有发现任何区别。

更新:

如果有人想尝试并行加载数据,请参见下面的并行加载版本。从理论上讲,如果您的IO设备具有足够的并行容量(通常是SSD),这可能会使它运行得更快。从字节创建Doubles还存在一些开销,因此并行执行的速度可能也会更快。在我的系统上(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));
       }
   }
}

更新2:

我在12台核心开发机器中的一台上执行了代码,并稍作修改以设置固定数量的核心。得到以下结果:

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

在此系统上,我还尝试了Python版本(占用1m2.994s)和Zjarek的C ++版本(占1.925s)(由于某些原因,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.968s。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.044s。我什至没有尝试过Python。

C ++版本与其结果非常一致,其中Java稍有波动。首先,当问题变大时,它的效率会提高一点,但随后效率会降低。


1
此代码无法为我正确解析double值。是否需要Java 7才能正确解析文件中的值?
jonderry 2011年

1
啊,愚蠢的我。在将IO代码从多行本地重构为一行后,我忘记再次设置字节序。通常需要Java 7,除非您向Java 6单独添加了fork / join。
arjan 2011年

我机器上的3.411秒。不错,但是比koumes21的java解决方案慢:)
static_rtti 2011年

1
我将在本地尝试koumes21的解决方案,以查看系统上的相对差异是什么。无论如何,从koumes21中“松散”不会感到羞耻,因为这是一个更聪明的解决方案。这只是扔进fork / join池中的几乎标准的快速排序;)
arjan 2011年

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实现,它试图真正利用已知的发行版。它基本上使用分布信息执行一轮存储桶排序,然后在每个存储桶上进行几轮快速排序,并假设在存储桶的限制范围内进行均匀分布,最后进行修改后的选择排序以将数据复制回原始缓冲区。快速排序会记住拆分点,因此选择排序仅需要对小块进行操作。尽管有这么多的复杂性,但它并不是真的很快。

为了快速评估Φ,在几个点上对值进行采样,然后仅使用线性插值。实际上,只要近似严格是单调的,就可以准确地估计Φ并不重要。

选择料斗大小,以使料斗溢出的机会可以忽略不计。更准确地说,使用当前参数,包含50000000个元素的数据集将导致垃圾箱溢出的机会是3.65e-09。(这可以通过使用计算生存函数的的泊松分布)。

要编译,请使用

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

由于比其他解决方案有更多的计算量,因此需要这些编译器标志来使其至少合理地快。如果不-msse3从转换doubleint变得很慢。如果您的体系结构不支持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.098秒!我必须添加-lm才能对其进行编译(用于erf)。
static_rtti 2011年

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()将每个元素适当地放入一个bin中,然后对每个bin进行排序。它将阵列完全保留在原位。

第一遍:docensus()计算每个bin中的元素数量。

第二遍:partition()置换数组,将每个元素放入其适当的bin中

第三遍:sortbins()在每个bin上执行qsort。

这有点天真,并且为每个值两次调用昂贵的erf()函数。第一遍和第三遍可能并行化。第二个是高度串行的,可能由于其高度随机的内存访问模式而减慢了速度。根据CPU功率与内存速度之比,可能还值得缓存每个双精度对象的bin数量。

该程序使您可以选择要使用的垃圾箱数量。只需在命令行中添加第二个数字即可。我用gcc -O3编译了它,但是我的机器太弱了,我不能告诉你任何好的性能数字。

编辑: of!我的C程序已使用std :: sort神奇地转换为C ++程序!


您可以使用phi以获得更快的stdnormal_cdf。
Alexandru

我应该放几箱?
static_rtti 2011年

@Alexandru:我向normcdf添加了分段线性逼近,但速度仅提高了5%。
弗洛伊德2011年

@static_rtti:您不必放任何东西。默认情况下,代码选择bin计数,因此平均bin大小为128kb的10/11。垃圾箱太少,您将无法获得分区的好处。太多,分区阶段由于缓存溢出而陷入困境。
弗洛伊德2011年

10.6秒!我尝试了一些箱的数量,使用5000获得了最好的结果(略高于默认值3356)。我必须说我应该为您的解决方案看到更好的性能……也许这是您使用qsort而不是可能更快的C ++解决方案std :: sort的事实吗?
static_rtti 2011年

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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.