【问题标题】:Fastest way to sort a big array of Gaussian-distributed data对大量高斯分布数据进行排序的最快方法
【发布时间】:2011-09-10 01:36:25
【问题描述】:

随着对this question 的兴趣,我认为通过提出竞赛来使答案更加客观和量化会很有趣。

这个想法很简单:我生成了一个包含 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 可用。

请注意,比赛的目的是看看我们是否可以使用有关数据的先前信息。它不应该是不同编程语言实现之间的匹配!

【问题讨论】:

  • 获取每个值并将其直接移动到其“预期”位置,重复移位值。不知道如何解决几个问题。完成后,冒泡排序直到完成(应该做几次)。
  • 如果到那时还没有关闭,我将在明天晚上发布一个桶排序解决方案:)
  • @static_rtti - 作为一个重度 CG 用户,这正是“我们”喜欢在 CG.SE 上破解的东西。对于任何阅读模式,将其移至 CG,不要关闭它。
  • 欢迎来到 CodeGolf.SE!我已经从 SO 原版中清除了很多关于它属于或不属于哪里的评论,并重新标记为更接近 CodeGolf.SE 主流。
  • 这里的一个棘手问题是我们寻找 objective 获胜标准,而“最快”引入了平台依赖性......执行 O(n^{1.2}) 算法在 cpython 虚拟机上实现了一个 O(n^{1.3}) 算法,在 c 中实现了类似的常量?我通常建议对每个解决方案的性能特征进行一些讨论,因为这可以帮助人们判断正在发生的事情。

标签: sorting fastest-code restricted-memory


【解决方案1】:

这是 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 效果最好。
  • 为什么我选择256作为bucket的个数?我尝试了 128 和 512,但 256 效果最好。 :) 我凭经验发现了它,我不确定为什么增加存储桶的数量会减慢算法的速度——内存分配不应该花那么长时间。也许与缓存大小有关?
  • 2.725s 在我的机器上。考虑到 JVM 的加载时间,这对于 java 解决方案来说非常好。
  • 根据我和 Arjan 的解决方案(使用他的语法,因为它比我的更简洁),我将您的代码切换为使用 nio 包,并且能够以 0.3 秒的速度获得它。我有一个ssd,我想知道如果没有可能会产生什么影响。它也摆脱了你的一些小玩意。修改部分为here.
  • 这是my tests (16core cpu) 上最快的并行解决方案。距离第二名的 1.94 秒相差 1.22 秒。
【解决方案2】:

不要变得聪明,只是为了提供一个更快的幼稚排序器,这里有一个 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 花费的时间要少一分钟多:与 87 秒相比,大约需要 11 秒。

【讨论】:

  • 在我的机器上用了 10.086 秒,这让你成为了当前的领导者!但我很确定我们可以做得更好:)
  • 您能否尝试删除第二个三元运算符并在这种情况下简单地返回 1,因为在这些数据量中,随机双精度数彼此不相等。
  • @Codism:我要补充一点,我们不关心交换等效数据的位置,因此即使我们可以获得等效值,这也是一个适当的简化。
【解决方案3】:

我根据最好将其分解为 4 的标准差进行了分段。编辑:根据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 gigs 的 ram 运行它并得到了大约相同的时间)

编辑:删除了 allocateDirect,比仅分配慢。删除了数组缓冲区的初始大小。实际上使它读取了整个 50000000 个值。重写以希望避免自动装箱问题(仍然比 naive 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());

  }
}

【讨论】:

  • 8.185s!很好的 scala 解决方案,我猜......另外,为提供第一个以某种方式实际使用高斯分布的解决方案而喝彩!
  • 我只是想与 c# 解决方案竞争。没想到我会打败 c/c++。另外..它对您的行为与对我的行为大不相同。我最终使用的是openJDK,而且速度要慢得多。我想知道添加更多分区是否会对您的环境有所帮助。
【解决方案4】:

只要把这个放到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#然后运行。
  • 更新为使用四个线程来提高性能。现在给我 6 秒。请注意,如果您只使用一个备用阵列并避免将大量内存初始化为零(这是由 CLR 完成的),这可能会得到显着改善(可能需要 5 秒),因为所有内容都至少被写入一次。
  • 9.598s 在我的机器上!你是当前的领导者:)
  • 我妈妈告诉我要远离使用 Mono 的人!
【解决方案5】:

既然您知道分布是什么,您可以使用直接索引 O(N) 排序。 (如果您想知道那是什么,假设您有一副 52 张卡片,并且您想对其进行分类。只需有 52 个垃圾箱,然后将每张卡片扔到自己的垃圾箱中。)

你有 5e7 双打。分配一个 5e7 双精度的结果数组 R。取每个号码x 并得到i = phi(x) * 5e7。基本上做R[i] = x。有一种方法来处理冲突,例如移动它可能发生冲突的数字(如简单的哈希编码)。或者,您可以将 R 放大几倍,并用唯一的 empty 值填充。 最后,你只是扫了R的元素。

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

【讨论】:

  • 小心:您知道的是近似分布,而不是确切分布。您知道数据是使用高斯定律生成的,但由于它是有限的,它并不完全遵循高斯。
  • @static_rtti:在这种情况下,必要的 phi 近似值会比 IMO 数据集中的任何异常情况造成更大的麻烦。
  • @static_rtti:不一定要准确。它只需要分散数据,使其大致均匀,因此在某些地方不会聚集太多。
  • 假设你有 5e7 双打。为什么不将 R 中的每个条目都设为一个向量,比如 5e6 个双精度向量。然后,push_back 每个双精度数在其适当的向量中。对向量进行排序,你就完成了。这应该花费输入大小的线性时间。
  • 其实我看到 mdkess 已经想出了那个解决方案。
【解决方案6】:

这是另一个顺序解决方案:

#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

请注意,此解决方案具有线性时间复杂度(因为它使用双精度的特殊表示)。

编辑:修正了元素的顺序增加。

编辑:将速度提高了近半秒。

编辑:速度再提高 0.7 秒。使算法对缓存更友好。

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

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

【讨论】:

  • 现在变得有趣了!是什么样的排序算法?
  • Least significant digit radix sort。您可以对尾数、指数、符号进行排序。这里介绍的算法将这一想法更进一步。它可以使用不同答案中提供的分区思想进行并行化。
  • 单线程解决方案相当快:2.552s!你认为你可以改变你的解决方案来利用数据是正态分布的事实吗?您可能比目前最好的多线程解决方案做得更好。
  • @static_rtti:我看到大马士革钢铁公司已经发布了这个实现的多线程版本。我改进了这个算法的缓存行为,所以你现在应该得到更好的时机。请测试这个新版本。
  • 1.459s 在我的最新测试中。虽然按照我的规则,这个解决方案不是赢家,但它确实值得称赞。恭喜!
【解决方案7】:

采用 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

【讨论】:

  • 2.958s! TBB 看起来很酷且易于使用!
  • TBB 太棒了。这正是算法工作的正确抽象级别。
【解决方案8】:

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

【讨论】:

  • 这实际上就是我在我的身上所做的......当然,在我完成我的写作之前你已经发布了这篇文章。
【解决方案9】:

非常难看(当我可以使用以数字结尾的变量时为什么要使用数组),但是快速代码(我第一次尝试 std::threads),在我的系统上的整个时间(真实时间)1,8 秒(与 std 相比::sort() 4,8 s),使用 g++ -std=c++0x -O3 -march=native -pthread 编译 只需通过标准输入传递数据(仅适用于 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文件。

【讨论】:

  • 你能把它改成 gaussian.dat,就像上面的 C++ 解决方案一样吗?
  • 等我回家再试试。
  • 非常好的解决方案,你是当前的领导者(1.949s)!并且很好地使用了高斯分布:)
【解决方案10】:

使用 std::sort 的 C++ 解决方案(最终比 qsort 更快,关于 Performance of qsort vs 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 代码,我只能制作一个只有 25mio 双精度的gaussian.dat 文件(没有出现内存错误)。但我很感兴趣 std::sort 算法运行多长时间。

【讨论】:

  • 6.425s!正如预期的那样,C++ 大放异彩:)
  • @static_rtti:我已经尝试过 swensons Timsort 算法(正如 Matthieu M. 在您的第一个 question 中所建议的那样)。我必须对 sort.h 文件进行一些更改才能使用 C++ 编译它。它比std::sort 慢大约两倍。不知道为什么,可能是因为编译器优化?
【解决方案11】:

这是 Alexandru 的基数排序与 Zjarek 的螺纹智能旋转的混合。编译它

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

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

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

sorter_gaussian_radix 50000000 1

如果你有 16 个核心

sorter_gaussian_radix 50000000 4

现在的最大深度是 6(64 个线程)。如果你放太多关卡,你只会拖慢代码。

我还尝试过来自英特尔性能基元 (IPP) 库的基数排序。 Alexandru 的实现完全击败了 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%。

编辑:修复了一个导致超过 2 个核心时效率低下的标志错误。

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

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

【讨论】:

  • 不错。基数排序对缓存非常不友好。看看你是否可以通过更改step 获得更好的结果(11 在我的笔记本电脑上是最佳的)。
  • 你有一个错误:int cnt[mask] 应该是int cnt[mask + 1]。为了获得更好的结果,请使用固定值 int cnt[1 &lt;&lt; 16]
  • 我今天晚些时候回家后会尝试所有这些解决方案。
  • 1.534s!!!我认为我们有一个领导者:-D
  • @static_rtti:你能再试一次吗?它比您上次尝试时快得多。在我的机器上,它比任何其他解决方案都快得多。
【解决方案12】:

我想这真的取决于你想做什么。如果你想对一堆高斯进行排序,那么这对你没有帮助。但是如果你想要一堆排序的高斯,这会。即使这有点忽略了问题,我认为将其与实际的排序例程进行比较会很有趣。

如果你想让某件事变得快,那就少做点。

您可以从正态分布中按排序顺序生成一堆样本,而不是从正态分布中生成一堆随机样本然后排序。

您可以使用解决方案here 来生成 n 个有序的均匀随机数。然后您可以使用正态分布的逆 cdf (scipy.stats.norm.ppf) 将均匀随机数通过inverse transform sampling 转换为正态分布中的数字。

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 计算,并使用之前的结果作为你的初始猜测。由于猜测会非常接近,因此一次迭代可能会给您带来很高的准确性。

【讨论】:

  • 不错的答案,但那会作弊 :) 我的问题的想法是,虽然排序算法得到了极大的关注,但几乎没有关于使用关于数据的先验知识进行排序的文献,尽管已经解决该问题的少数论文报告了不错的收益。那么让我们看看有什么可能!
【解决方案13】:

用这个 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。稍微快一点:)
【解决方案14】:

既然你知道分布,我的想法是制作 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 快很多。

【讨论】:

  • 我认为这是最好的解决方案。
  • 您并不确切知道最终将包含在存储桶中的元素数量,因此数学实际上是错误的。话虽如此,我认为这是一个很好的答案。
  • @pouejapon:你是对的。
  • 这个答案听起来真的很好。问题是 - 它并不是真的很快。我在 C99 中实现了这个(见我的回答),它当然很容易击败 std::sort(),但它比 Alexandru 的 radixsort 解决方案慢得多。
【解决方案15】:

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

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

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

如今,计算比内存访问便宜得多。但是我们有大量的项目,所以当哑缓存感知排序比低复杂度的非缓存感知版本慢时,很难判断哪个是数组大小。

但我不会提供上述的实现,因为我会在 Windows (VC++) 中实现。

【讨论】:

    【解决方案16】:

    这是一个线性扫描桶排序实现。我认为它比除基数排序之外的所有当前单线程实现都快。如果我足够准确地估计 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;
    }
    

    【讨论】:

    • 我今天晚些时候回家后试试这个。同时,我可以说你的代码很丑吗? :-D
    • 3.071 秒!对于单线程解决方案来说还不错!
    【解决方案17】:

    我不知道,为什么我不能编辑我以前的帖子,所以这是新版本,速度快了 0.2 秒(但 CPU 时间(用户)快了大约 1.5 秒)。该解决方案有 2 个程序,首先为桶排序预先计​​算正态分布的分位数,并将其存储在表中,t[double * scale] = bucket index,其中 scale 是一些任意数字,这使得转换为 double 成为可能。然后主程序可以使用这些数据将双打放入正确的桶中。它有一个缺点,如果数据不是高斯数据,它将无法正常工作(而且对于正态分布,错误工作的可能性几乎为零),但是针对特殊情况的修改既简单又快速(只有桶数检查并下降到 std ::种类())。

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

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

    【讨论】:

    • 1.621s!我认为你是领导者,但我很快就失去了所有这些答案的轨道:)
    【解决方案18】:

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

    算法是这样的:

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

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

    【讨论】:

    • 2.470 秒!非常好的想法。如果想法很有趣,解决方案不是最快的也没关系:)
    • 这和我的一样,但是将 phi 计算和移位组合在一起以获得更好的缓存性能,对吧?
    • @jonderry:我赞成你的解决方案,现在我明白它的作用了。不是想偷你的主意。我将您的实现包含在我的(非官方)set of tests
    【解决方案19】:

    我个人最喜欢使用 Intel 的 Threaded Building Blocks 已经发布,但这里有一个使用 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;
                }
            }
        }    
    }
    

    重要免责声明:我从:https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel

    获取了 fork/join 的快速排序改编

    要运行它,您需要 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 通常有),这可以让它运行得更快一些。从字节创建双精度也有一些开销,因此并行速度也可能更快。在我的系统(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
    

    在这个系统上,我还尝试了耗时 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.044s。我什至没有尝试过 Python。

    C++ 版本与其结果非常一致,Java 略有波动。首先,当问题变大时效率会更高一些,但随后效率会再次降低。

    【讨论】:

    • 这段代码没有为我正确解析双精度值。是否需要 Java 7 才能正确解析文件中的值?
    • 啊,我真傻。在本地将 IO 代码从多行重构为一行后,我忘记再次设置字节顺序。通常需要 Java 7,除非您当然将 fork/join 单独添加到 Java 6。
    • 3.411s 在我的机器上。不错,但比 koumes21 的 java 解决方案慢 :)
    • 我也会在本地尝试 koumes21 的解决方案,以查看我的系统上的相对差异。无论如何,从 koumes21 中“失去”并不丢人,因为这是一个更聪明的解决方案。这只是扔进 fork/join 池中的几乎标准的快速排序;)
    【解决方案20】:

    使用传统 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
    

    【讨论】:

      【解决方案21】:

      这是一个尝试真正利用已知分布的顺序 C99 实现。它基本上使用分布信息进行单轮桶排序,然后在每个桶上进行几轮快速排序,假设在桶的范围内均匀分布,最后修改选择排序以将数据复制回原始缓冲区。快速排序记住了分割点,所以选择排序只需要对小块进行操作。尽管(因为?)所有这些复杂性,它甚至都不是很快。

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

      容器大小的选择使得容器溢出的可能性可以忽略不计。更准确地说,使用当前参数,50000000 个元素的数据集导致 bin 溢出的机会是 3.65e-09。 (这可以使用Poisson distributionsurvival function 来计算。)

      要编译,请使用

      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)。
      【解决方案22】:
      #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 功率与内存速度的比率,缓存每个 double 的 bin 编号也可能是值得的。

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

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

      【讨论】:

      • 您可以使用 phi 获得更快的 stdnormal_cdf。
      • 我应该放多少个箱子,大约?
      • @Alexandru:我在 normcdf 中添加了分段线性逼近,但速度仅提高了 5%。
      • @static_rtti:你不必放任何东西。默认情况下,代码选择 bin 计数,因此平均 bin 大小为 128kb 的 10/11。垃圾箱太少,您无法享受分区带来的好处。太多,分区阶段由于缓存溢出而陷入困境。
      • 10.6s!我尝试使用 bin 的数量进行一些操作,我得到了 5000 的最佳结果(略高于默认值 3356)。我必须说,我应该为您的解决方案看到更好的性能......也许是因为您使用的是 qsort 而不是 C++ 解决方案中可能更快的 std::sort ?
      【解决方案23】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-08-30
        • 2019-06-22
        • 1970-01-01
        • 2011-04-25
        • 1970-01-01
        • 1970-01-01
        • 2011-10-14
        • 2017-09-14
        相关资源
        最近更新 更多