Cuộc thi: cách nhanh nhất để sắp xếp một mảng lớn dữ liệu phân phối Gaussian


71

Theo sau sự quan tâm đến câu hỏi này , tôi nghĩ sẽ rất thú vị khi đưa ra câu trả lời khách quan và định lượng hơn một chút bằng cách đề xuất một cuộc thi.

Ý tưởng rất đơn giản: Tôi đã tạo ra một tệp nhị phân chứa 50 triệu nhân đôi phân phối gaussian (trung bình: 0, stdev 1). Mục tiêu là tạo ra một chương trình sắp xếp những thứ này trong bộ nhớ nhanh nhất có thể. Một triển khai tham chiếu rất đơn giản trong python mất 1m4 để hoàn thành. chúng ta có thể đi cúi thấp bao nhiêu?

Các quy tắc như sau: trả lời với một chương trình mở tệp "gaussian.dat" và sắp xếp các số trong bộ nhớ (không cần xuất chúng) và hướng dẫn xây dựng và chạy chương trình. Chương trình phải có khả năng hoạt động trên máy Arch Linux của tôi (có nghĩa là bạn có thể sử dụng bất kỳ ngôn ngữ lập trình hoặc thư viện nào có thể cài đặt dễ dàng trên hệ thống này).

Chương trình phải có thể đọc được một cách hợp lý, để tôi có thể chắc chắn rằng nó an toàn để khởi chạy (vui lòng không có giải pháp chỉ dành cho trình biên dịch!).

Tôi sẽ chạy các câu trả lời trên máy của mình (lõi tứ, RAM 4 GB). Giải pháp nhanh nhất sẽ nhận được câu trả lời được chấp nhận và tiền thưởng 100 điểm :)

Chương trình được sử dụng để tạo các số:

#!/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)

Việc thực hiện tham khảo đơn giản:

#!/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)

EDIT: chỉ có 4 GB RAM, xin lỗi

EDIT # 2: Lưu ý rằng điểm của cuộc thi là để xem liệu chúng tôi có thể sử dụng thông tin trước về dữ liệu hay không . nó không phải là một trận đấu tức giận giữa các triển khai ngôn ngữ lập trình khác nhau!


1
Lấy từng giá trị và di chuyển trực tiếp đến vị trí "mong đợi" của nó, lặp lại cho giá trị được dịch chuyển. Không chắc chắn làm thế nào để giải quyết một vài vấn đề với điều đó. Khi hoàn thành, sắp xếp bong bóng cho đến khi hoàn thành (một vài lần nên làm).

1
Tôi sẽ đăng một giải pháp sắp xếp xô vào tối mai nếu điều này chưa được đóng lại sau đó :)

1
@static_rtti - với tư cách là một người dùng CG nặng, đây chính xác là thứ mà "chúng tôi" muốn hack tại CG.SE. Đối với bất kỳ mod đọc nào, hãy chuyển nó sang CG, đừng đóng nó.
arrdem

1
Chào mừng bạn đến với CodeGolf.SE! Tôi đã xóa rất nhiều bình luận từ bản gốc SO liên quan đến việc cái này không hoặc không thuộc về, và được gắn thẻ lại để gần với dòng chính CodeGolf.SE hơn.
dmckee

2
Một vấn đề khó khăn ở đây là chúng tôi tìm kiếm các tiêu chí chiến thắng khách quan và "nhanh nhất" giới thiệu các phụ thuộc nền tảng ... thuật toán O (n ^ {1.2}) được triển khai trên máy ảo cpython đánh bại O (n ^ {1.3} ) thuật toán với hằng số tương tự được thực hiện trong c? Tôi thường đề xuất một số thảo luận về đặc điểm hiệu suất của từng giải pháp, vì điều này có thể giúp mọi người đánh giá những gì đang diễn ra.
dmckee

Câu trả lời:


13

Đây là một giải pháp trong C ++, trước tiên phân vùng các số thành các nhóm có cùng số phần tử dự kiến ​​và sau đó sắp xếp từng nhóm riêng biệt. Nó tính toán trước một bảng của hàm phân phối tích lũy dựa trên một số công thức từ Wikipedia và sau đó nội suy các giá trị từ bảng này để có được xấp xỉ nhanh.

Một số bước chạy trong nhiều luồng để sử dụng bốn lõi.

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

Để biên dịch và chạy nó, sử dụng lệnh này:

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

EDIT: Tất cả các nhóm bây giờ được đặt vào cùng một mảng để loại bỏ nhu cầu sao chép các nhóm trở lại vào mảng. Ngoài ra kích thước của bảng với các giá trị được tính toán trước đã giảm, vì các giá trị này đủ chính xác. Tuy nhiên, nếu tôi thay đổi số lượng xô trên 256, chương trình sẽ mất nhiều thời gian hơn so với số lượng xô đó.

EDIT: Cùng một thuật toán, ngôn ngữ lập trình khác nhau. Tôi đã sử dụng C ++ thay vì Java và thời gian chạy giảm từ ~ 3.2s xuống ~ 2.35 trên máy của tôi. Số lượng xô tối ưu vẫn là khoảng 256 (một lần nữa, trên máy tính của tôi).

Nhân tiện, tbb thực sự là tuyệt vời.

EDIT: Tôi đã được truyền cảm hứng từ giải pháp tuyệt vời của Alexandru và thay thế std :: sort trong giai đoạn cuối bằng một phiên bản sửa đổi của loại cơ số của anh ấy. Tôi đã sử dụng một phương pháp khác để xử lý các số dương / âm, mặc dù nó cần nhiều hơn thông qua mảng. Tôi cũng quyết định sắp xếp mảng chính xác và loại bỏ sắp xếp chèn. Sau này tôi sẽ dành thời gian để kiểm tra những thay đổi này ảnh hưởng đến hiệu suất và có thể hoàn nguyên chúng như thế nào. Tuy nhiên, bằng cách sử dụng sắp xếp cơ số, thời gian giảm từ ~ 2,35 giây xuống ~ 1,63 giây.


Đẹp. Tôi đã nhận được 3.055 của tôi. Thấp nhất tôi có thể lấy của tôi là 6,3. Tôi đang chọn thông qua của bạn để có được số liệu thống kê tốt hơn. Tại sao bạn chọn 256 làm số xô? Tôi đã thử 128 và 512, nhưng 256 hoạt động tốt nhất.
Scott

Tại sao tôi chọn 256 làm số xô? Tôi đã thử 128 và 512, nhưng 256 hoạt động tốt nhất. :) Tôi đã tìm thấy nó theo kinh nghiệm và tôi không chắc tại sao việc tăng số lượng xô làm chậm thuật toán - việc cấp phát bộ nhớ sẽ không mất nhiều thời gian như vậy. Có lẽ một cái gì đó liên quan đến kích thước bộ đệm?
k21

2.725s trên máy của tôi. Khá hay cho một giải pháp java, có tính đến thời gian tải của JVM.
static_rtti

2
Tôi đã chuyển mã của bạn sang sử dụng các gói nio, theo giải pháp của tôi và Arjan (đã sử dụng cú pháp của anh ấy, vì nó sạch hơn mã của tôi) và có thể lấy nó nhanh hơn .3 ​​giây. Tôi đã có một ssd, tôi tự hỏi ý nghĩa của nó có thể là gì nếu không. Nó cũng được loại bỏ một số twiddling của bạn. Phần modded ở đây.
Scott

3
Đây là giải pháp song song nhanh nhất trong các thử nghiệm của tôi (cpu 16 lõi). 1,22s xa vị trí thứ hai 1,94.
Alexandru

13

Không cần thông minh, chỉ để cung cấp một trình sắp xếp ngây thơ nhanh hơn nhiều, đây là một trong C tương đương với Python của bạn:

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

Được biên dịch với gcc -O3, trên máy của tôi, việc này mất ít hơn một phút so với Python: khoảng 11 giây so với 87 giây.


1
Đã lấy 10.086 trên máy của tôi, giúp bạn trở thành người dẫn đầu hiện tại! Nhưng tôi khá chắc chắn rằng chúng ta có thể làm tốt hơn :)

1
Bạn có thể cố gắng loại bỏ toán tử ternary thứ hai và chỉ cần trả về 1 cho trường hợp đó bởi vì các nhân đôi ngẫu nhiên giống như không bằng nhau trong số lượng dữ liệu này.
Codism

@Codism: Tôi sẽ nói thêm rằng chúng tôi không quan tâm đến việc hoán đổi vị trí của dữ liệu tương đương, do đó ngay cả khi chúng tôi có thể nhận được các giá trị tương đương thì đó cũng là một sự đơn giản hóa phù hợp.

10

Tôi phân vùng thành các phân đoạn dựa trên độ lệch chuẩn nên chia nó thành 4 giây. Chỉnh sửa: Viết lại thành phân vùng dựa trên giá trị x trong http://en.wikipedia.org/wiki/Error_feft#Table_of_values

http://www.wolframalpha.com/input/?i=percentages+by++n normal + phân phối

Tôi đã thử sử dụng các thùng nhỏ hơn, nhưng dường như nó chỉ có hiệu quả một lần 2 * ngoài số lượng lõi có sẵn. Nếu không có bất kỳ bộ sưu tập song song nào, sẽ mất 37 giây trên hộp của tôi và 24 với các bộ sưu tập song song. Nếu phân vùng thông qua phân phối, bạn không thể chỉ sử dụng một mảng, do đó có thêm một số chi phí. Tôi không rõ ràng khi nào một giá trị sẽ được đóng hộp / bỏ hộp trong scala.

Tôi đang sử dụng scala 2.9, cho bộ sưu tập song song. Bạn chỉ có thể tải xuống bản phân phối tar.gz của nó.

Để biên dịch: scalac SortFile.scala (Tôi chỉ sao chép nó trực tiếp trong thư mục scala / bin.

Để chạy: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (Tôi đã chạy nó với 2 hợp đồng ram và nhận được cùng một lúc)

Chỉnh sửa: Đã xóa allocateDirect, chậm hơn so với chỉ phân bổ. Loại bỏ mồi kích thước ban đầu cho bộ đệm mảng. Thực tế làm cho nó đọc toàn bộ 50000000 giá trị. Viết lại để hy vọng tránh các vấn đề về hộp thư tự động (vẫn chậm hơn so với ngây thơ c)

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


object SortFile {

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

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

def partition(dbl:Double): Int = { 

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

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

}

  def main(args: Array[String])
    { 

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

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

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

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

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

  }
}

1
8.185s! Thật tuyệt cho một giải pháp scala, tôi đoán ... Ngoài ra, bravo đã cung cấp giải pháp đầu tiên thực sự sử dụng phân phối Gaussian theo một cách nào đó!

1
Tôi chỉ nhằm mục đích cạnh tranh với giải pháp c #. Không hình tôi sẽ đánh bại c / c ++. Ngoài ra .. nó đối xử với bạn khác nhiều so với tôi. Tôi đang sử dụng openJDK và nó chậm hơn rất nhiều. Tôi tự hỏi nếu thêm nhiều phân vùng sẽ giúp trong env của bạn.
Scott

9

Chỉ cần đặt nó trong một tệp cs và biên dịch nó với csc theo lý thuyết: (Yêu cầu đơn âm)

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();
        }
    }
}

Tôi có thể chạy các giải pháp của bạn với Mono không? Tôi nên làm thế nào?

Không sử dụng Mono, không nghĩ về điều đó, bạn sẽ có thể biên dịch F # và sau đó chạy nó.

1
Cập nhật để sử dụng bốn chủ đề để cải thiện hiệu suất. Bây giờ cho tôi 6 giây. Lưu ý rằng điều này có thể được cải thiện đáng kể (có thể là 5 giây) nếu bạn chỉ sử dụng một mảng dự phòng và tránh khởi tạo một tấn bộ nhớ về 0, được thực hiện bởi CLR, vì mọi thứ đều được ghi vào ít nhất một lần.

1
9.598s trên máy của tôi! Bạn là người lãnh đạo hiện tại :)

1
Mẹ tôi bảo tôi tránh xa những kẻ có Mono!

8

Vì bạn biết phân phối là gì, bạn có thể sử dụng sắp xếp O (N) lập chỉ mục trực tiếp. (Nếu bạn đang tự hỏi đó là gì, giả sử bạn có một bộ gồm 52 thẻ và bạn muốn sắp xếp nó. Chỉ cần có 52 thùng và ném mỗi thẻ vào thùng riêng của nó.)

Bạn có 5e7 nhân đôi. Phân bổ một mảng kết quả R là 5e7 nhân đôi. Lấy từng số xvà nhận i = phi(x) * 5e7. Về cơ bản làm R[i] = x. Có một cách để xử lý các va chạm, chẳng hạn như di chuyển số có thể va chạm (như trong mã băm đơn giản). Ngoài ra, bạn có thể làm cho R lớn hơn một vài lần, chứa đầy một giá trị trống duy nhất . Cuối cùng, bạn chỉ cần quét các phần tử của R.

phichỉ là hàm phân phối tích lũy gaussian. Nó chuyển đổi một số phân phối gaussian giữa +/- vô cực thành một số phân phối đồng đều giữa 0 và 1. Một cách đơn giản để tính toán nó là tra cứu bảng và nội suy.


3
Hãy cẩn thận: bạn biết phân phối gần đúng, không phải phân phối chính xác. Bạn biết dữ liệu được tạo bằng định luật Gaussian, nhưng vì nó là hữu hạn, nên nó không chính xác tuân theo Gaussian.

@static_rtti: Trong trường hợp này, xấp xỉ cần thiết của phi sẽ tạo ra rắc rối lớn hơn bất kỳ sự bất thường nào trong tập dữ liệu IMO.

1
@static_rtti: không cần phải chính xác. Nó chỉ phải trải ra dữ liệu để nó gần như thống nhất, vì vậy nó không bị bó quá nhiều ở một số nơi.

Giả sử bạn có 5e7 nhân đôi. Tại sao không chỉ làm cho mỗi mục trong R là một vectơ của 5e6, gấp đôi. Sau đó, đẩy_back mỗi đôi trong vectơ thích hợp của nó. Sắp xếp các vectơ và bạn đã hoàn tất. Điều này sẽ mất thời gian tuyến tính trong kích thước của đầu vào.
Neil G

Trên thực tế, tôi thấy rằng mdkess đã đưa ra giải pháp đó.
Neil G

8

Đây là một giải pháp tuần tự khác:

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

Tôi nghi ngờ nó đánh bại giải pháp đa luồng, nhưng thời gian trên máy tính xách tay i7 của tôi là (stdsort là giải pháp C ++ được cung cấp trong câu trả lời khác):

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

Lưu ý rằng giải pháp này có độ phức tạp thời gian tuyến tính (vì nó sử dụng biểu diễn đặc biệt của nhân đôi).

EDIT : Đã sửa lỗi thứ tự các phần tử sẽ tăng lên.

EDIT : Cải thiện tốc độ gần nửa giây.

EDIT : Cải thiện tốc độ thêm 0,7 giây. Làm cho thuật toán trở nên thân thiện hơn với bộ đệm.

EDIT : Cải thiện tốc độ thêm 1 giây. Vì chỉ có 50.000.000 phần tử, tôi có thể sắp xếp một phần lớp phủ và sử dụng insert sort (thân thiện với bộ đệm) để sửa các phần tử không đúng chỗ. Ý tưởng này loại bỏ khoảng hai lần lặp từ vòng lặp sắp xếp cơ số cuối cùng.

EDIT : 0,16 ít giây. Đầu tiên std :: Reverse có thể được loại bỏ nếu thứ tự sắp xếp bị đảo ngược.


Bây giờ điều đó đang trở nên thú vị! Đó là loại thuật toán sắp xếp nào?
static_rtti

2
Ít nhất là chữ số radix sắp xếp . Bạn có thể sắp xếp lớp phủ, sau đó là số mũ, sau đó là dấu hiệu. Thuật toán được trình bày ở đây đưa ý tưởng này tiến thêm một bước. Nó có thể được song song bằng cách sử dụng một ý tưởng phân vùng được cung cấp trong một câu trả lời khác nhau.
Alexandru

Khá nhanh cho một giải pháp đơn luồng: 2.552s! Bạn có nghĩ rằng bạn có thể thay đổi giải pháp của mình để sử dụng thực tế là dữ liệu được phân phối bình thường không? Bạn có thể có thể làm tốt hơn các giải pháp đa luồng tốt nhất hiện tại.
static_rtti

1
@static_rtti: Tôi thấy rằng Damascus Steel đã đăng một phiên bản đa luồng của việc triển khai này. Tôi đã cải thiện hành vi lưu trữ của thuật toán này, vì vậy bạn sẽ có được thời gian tốt hơn bây giờ. Vui lòng kiểm tra phiên bản mới này.
Alexandru

2
1.459s trong các bài kiểm tra mới nhất của tôi. Mặc dù giải pháp này không phải là người chiến thắng theo quy tắc của tôi, nhưng nó thực sự xứng đáng với danh tiếng lớn. Xin chúc mừng!
static_rtti

6

Lấy giải pháp của Christian Ammer và song song hóa nó với Khối xây dựng có ren của Intel

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

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

Nếu bạn có quyền truy cập vào thư viện Hiệu suất nguyên thủy (IPP) của Intel, bạn có thể sử dụng loại cơ số của nó. Chỉ cần thay thế

#include <tbb/parallel_sort.h>

với

#include "ipps.h"

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

với

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

Trên máy tính xách tay lõi kép của tôi, thời gian là

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 giây! TBB có vẻ khá tuyệt và dễ sử dụng!

2
TBB là tuyệt vời vô lý. Đó chính xác là mức độ trừu tượng phù hợp cho công việc thuật toán.
drxzcl

5

Làm thế nào về việc triển khai quicksort song song chọn các giá trị trục của nó dựa trên số liệu thống kê phân phối, do đó đảm bảo các phân vùng có kích thước bằng nhau? Trục đầu tiên sẽ ở mức trung bình (không trong trường hợp này), cặp tiếp theo sẽ ở phần trăm thứ 25 và 75 (+/- -0.67449 độ lệch chuẩn), v.v., với mỗi phân vùng giảm một nửa dữ liệu còn lại được đặt nhiều hơn hoặc kém hoàn hảo


Đó thực sự là những gì tôi đã làm với tôi .. tất nhiên bạn đã nhận được bài đăng này trước khi tôi có thể hoàn thành bài viết của mình.

5

Rất xấu (tại sao sử dụng mảng khi tôi có thể sử dụng các biến kết thúc bằng số), nhưng mã nhanh (lần đầu tiên tôi thử std :: thread), toàn bộ thời gian (thời gian thực) trên hệ thống của tôi 1,8 s (so với std :: sort () 4,8 s), biên dịch với g ++ -std = c ++ 0x -O3 -march = local -pthread Chỉ cần truyền dữ liệu qua stdin (chỉ hoạt động trong 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);
}

// Chỉnh sửa thay đổi để đọc tệp gaussian.dat.


Bạn có thể thay đổi nó để đọc gaussian.dat, như các giải pháp C ++ ở trên không?

Tôi sẽ thử sau khi tôi về nhà.
static_rtti

Giải pháp rất hay, bạn là người lãnh đạo hiện tại (1.949s)! Và sử dụng tốt phân phối gaussian :)
static_rtti

4

Một giải pháp C ++ sử dụng std::sort(cuối cùng nhanh hơn qsort, liên quan đến Hiệu suất của qsort so với 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;
}

Tôi không thể tin được bao lâu vì tôi chỉ có 1GB trên máy của mình và với mã Python đã cho, tôi chỉ có thể tạo một gaussian.dattệp chỉ với 25 triệu đôi (mà không gặp lỗi Bộ nhớ). Nhưng tôi rất quan tâm thuật toán std :: sort chạy trong bao lâu.


6.425s!

@static_rtti: Tôi đã thử thuật toán Timsort của swensons (như được đề xuất từ ​​Matthieu M. trong câu hỏi đầu tiên của bạn ). Tôi đã phải thực hiện một số thay đổi cho sort.htập tin để biên dịch nó với C ++. Nó chậm hơn khoảng hai lần std::sort. Không biết tại sao, có thể vì tối ưu hóa trình biên dịch?
Christian Ammer

4

Dưới đây là sự pha trộn của loại cơ số của Alexandru với trục xoay thông minh có ren của Zjarek. Biên dịch nó với

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

Bạn có thể thay đổi kích thước cơ số bằng cách xác định BƯỚC (ví dụ: thêm -DSTEP = 11). Tôi tìm thấy tốt nhất cho máy tính xách tay của tôi là 8 (mặc định).

Theo mặc định, nó chia vấn đề thành 4 phần và chạy nó trên nhiều luồng. Bạn có thể thay đổi điều đó bằng cách chuyển một tham số độ sâu cho dòng lệnh. Vì vậy, nếu bạn có hai lõi, hãy chạy nó như

sorter_gaussian_radix 50000000 1

và nếu bạn có 16 lõi

sorter_gaussian_radix 50000000 4

Độ sâu tối đa ngay bây giờ là 6 (64 luồng). Nếu bạn đặt quá nhiều cấp độ, bạn sẽ chỉ làm chậm mã.

Một điều tôi cũng đã thử là loại radix từ thư viện Intel Performance Primitive (IPP). Việc triển khai của Alexandru hoàn toàn đánh bại IPP, với việc IPP chậm hơn khoảng 30%. Sự thay đổi đó cũng được bao gồm ở đây (nhận xét ra).

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

EDIT : Tôi đã triển khai các cải tiến bộ nhớ cache của Alexandru và điều đó đã loại bỏ khoảng 30% thời gian trên máy của tôi.

EDIT : Điều này thực hiện một loại đệ quy, do đó, nó sẽ hoạt động tốt trên máy 16 lõi của Alexandru. Nó cũng sử dụng cải tiến cuối cùng của Alexandru và loại bỏ một trong những điều ngược lại. Đối với tôi, điều này đã cải thiện 20%.

EDIT : Đã sửa lỗi dấu hiệu gây ra sự kém hiệu quả khi có nhiều hơn 2 lõi.

EDIT : Đã xóa lambda, vì vậy nó sẽ biên dịch với các phiên bản cũ hơn của gcc. Nó bao gồm biến thể mã IPP được nhận xét. Tôi cũng đã sửa tài liệu để chạy trên 16 lõi. Theo như tôi có thể nói, đây là cách thực hiện nhanh nhất.

EDIT : Đã sửa lỗi khi BƯỚC không 8. Tăng số luồng tối đa lên 64. Đã thêm một số thông tin về thời gian.


Đẹp. Radix sort rất không thân thiện với bộ nhớ cache. Xem nếu bạn có thể nhận được kết quả tốt hơn bằng cách thay đổi step(11 là tối ưu trên máy tính xách tay của tôi).
Alexandru

Bạn có một lỗi: int cnt[mask]nên được int cnt[mask + 1]. Để có kết quả tốt hơn, sử dụng một giá trị cố định int cnt[1 << 16].
Alexandru

Tôi sẽ thử tất cả các giải pháp sau hôm nay khi tôi về nhà.
static_rtti

1.534 giây !!! Tôi nghĩ rằng chúng tôi có một nhà lãnh đạo :-D
static_rtti

@static_rtti: Bạn có thể thử lại không? Nó đã nhận được nhanh hơn đáng kể so với lần cuối bạn thử nó. Trên máy của tôi, nó nhanh hơn bất kỳ giải pháp nào khác.
Thép Damascus

2

Tôi đoán điều này thực sự phụ thuộc vào những gì bạn muốn làm. Nếu bạn muốn sắp xếp một loạt các Gaussian, thì điều này sẽ không giúp bạn. Nhưng nếu bạn muốn một loạt các Gaussian được sắp xếp, điều này sẽ. Ngay cả khi điều này bỏ lỡ vấn đề một chút, tôi nghĩ sẽ rất thú vị khi so sánh với các thói quen sắp xếp thực tế.

Nếu bạn muốn một cái gì đó để được nhanh chóng, làm ít hơn.

Thay vì tạo ra một loạt các mẫu ngẫu nhiên từ phân phối bình thường và sau đó sắp xếp, bạn có thể tạo một loạt các mẫu từ phân phối bình thường theo thứ tự được sắp xếp.

Bạn có thể sử dụng giải pháp ở đây để tạo n số ngẫu nhiên thống nhất theo thứ tự được sắp xếp. Sau đó, bạn có thể sử dụng cdf nghịch đảo (scipy.stats.norm.ppf) của phân phối bình thường để biến các số ngẫu nhiên thống nhất thành số từ phân phối bình thường thông qua lấy mẫu biến đổi nghịch đảo .

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))

Nếu bạn muốn làm cho bàn tay của mình trở nên bẩn hơn, tôi đoán rằng bạn có thể tăng tốc nhiều phép tính cdf nghịch đảo bằng cách sử dụng một số phương pháp lặp và sử dụng kết quả trước đó như dự đoán ban đầu của bạn. Vì các dự đoán sẽ rất gần, có lẽ một lần lặp duy nhất sẽ cho bạn độ chính xác cao.


2
Câu trả lời hay, nhưng đó sẽ là gian lận :) Ý tưởng của câu hỏi của tôi là trong khi các thuật toán sắp xếp đã được chú ý rất lớn, hầu như không có tài liệu nào về việc sử dụng kiến ​​thức trước về dữ liệu để sắp xếp, mặc dù có rất ít bài báo có Giải quyết vấn đề đã báo cáo lợi ích tốt đẹp. Vì vậy, hãy xem những gì có thể!

2

Hãy thử giải pháp thay đổi này của Guvante với Main () này, nó bắt đầu sắp xếp ngay sau khi đọc xong 1/4 IO, nó nhanh hơn trong thử nghiệm của tôi:

    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. Nhanh hơn một chút :)

2

Vì bạn biết phân phối, ý tưởng của tôi sẽ là tạo ra các thùng k, mỗi nhóm có cùng số lượng phần tử dự kiến ​​(vì bạn biết phân phối, bạn có thể tính toán này). Sau đó trong thời gian O (n), quét mảng và đặt các phần tử vào các thùng của chúng.

Sau đó đồng thời phân loại các thùng. Giả sử bạn có k xô và n phần tử. Một thùng sẽ mất (n / k) lg (n / k) thời gian để sắp xếp. Bây giờ giả sử rằng bạn có bộ xử lý p mà bạn có thể sử dụng. Vì các thùng có thể được sắp xếp độc lập, bạn có một hệ số trần (k / p) để xử lý. Điều này cung cấp thời gian chạy cuối cùng của n + ceil (k / p) * (n / k) lg (n / k), sẽ nhanh hơn so với n lg n nếu bạn chọn k tốt.


Tôi nghĩ rằng đây là giải pháp tốt nhất.
Neil G

Bạn không biết chính xác số lượng phần tử sẽ kết thúc trong một thùng, vì vậy toán học thực sự sai. Điều đó đang được nói, đây là một câu trả lời tốt, tôi nghĩ.
poulejapon

@pouejapon: Bạn nói đúng.
Neil G

Câu trả lời này nghe thực sự tốt đẹp. Vấn đề là - nó không thực sự nhanh. Tôi đã thực hiện điều này trong C99 (xem câu trả lời của tôi) và nó chắc chắn dễ dàng đánh bại std::sort(), nhưng nó chậm hơn so với giải pháp radixsort của Alexandru.
Sven Marnach

2

Một ý tưởng tối ưu hóa ở mức độ thấp là phù hợp với hai nhân đôi trong một thanh ghi SSE, vì vậy mỗi luồng sẽ hoạt động với hai mục cùng một lúc. Điều này có thể phức tạp để làm cho một số thuật toán.

Một điều cần làm là sắp xếp mảng trong các đoạn thân thiện với bộ đệm, sau đó hợp nhất các kết quả. Nên sử dụng hai mức: ví dụ 4 KB đầu tiên cho L1 sau đó là 64 KB cho L2.

Điều này sẽ rất thân thiện với bộ đệm, vì sắp xếp nhóm sẽ không đi ra ngoài bộ đệm và việc hợp nhất cuối cùng sẽ đi bộ nhớ theo tuần tự.

Ngày nay tính toán rẻ hơn nhiều so với truy cập bộ nhớ. Tuy nhiên, chúng tôi có một số lượng lớn các mục, vì vậy thật khó để biết đó là kích thước mảng khi sắp xếp nhận biết bộ đệm câm chậm hơn so với phiên bản không nhận biết bộ đệm có độ phức tạp thấp.

Nhưng tôi sẽ không cung cấp cách thực hiện ở trên vì tôi sẽ làm điều đó trong Windows (VC ++).


2

Đây là một triển khai sắp xếp xô quét tuyến tính. Tôi nghĩ rằng nó nhanh hơn tất cả các triển khai đơn luồng hiện tại ngoại trừ loại cơ số. Cần có thời gian chạy tuyến tính dự kiến ​​nếu tôi ước tính cdf đủ chính xác (Tôi đang sử dụng phép nội suy tuyến tính của các giá trị tôi tìm thấy trên web) và không mắc phải bất kỳ lỗi nào có thể gây quét quá mức:

#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
Tôi sẽ thử điều này sau hôm nay khi tôi về nhà. Trong khi đó, tôi có thể nói mã của bạn rất xấu không? :-D
static_rtti

3.071s! Không tệ cho một giải pháp đơn luồng!
static_rtti

2

Tôi không biết, tại sao tôi không thể chỉnh sửa bài đăng trước đây của mình, vì vậy đây là phiên bản mới, nhanh hơn 0,2 giây (nhưng nhanh hơn khoảng 1,5 giây về thời gian CPU (người dùng)). Giải pháp này có 2 chương trình, lần đầu tiên tính toán lượng tử để phân phối bình thường cho sắp xếp nhóm và lưu trữ nó trong bảng, t [double * scale] = chỉ số xô, trong đó tỷ lệ là một số tùy ý giúp cho việc đúc có thể tăng gấp đôi. Sau đó, chương trình chính có thể sử dụng dữ liệu này để đặt gấp đôi vào đúng thùng. Nó có một nhược điểm, nếu dữ liệu không phải là gaussian thì nó sẽ không hoạt động chính xác (và cũng gần như không có cơ hội hoạt động không chính xác cho phân phối bình thường), nhưng sửa đổi cho trường hợp đặc biệt là dễ dàng và nhanh chóng (chỉ số lần kiểm tra xô và rơi vào std ::Sắp xếp()).

Biên dịch: g ++ => http://pastebin.com/WG7pZEzH chương trình trợ giúp

g ++ -std = c ++ 0x -O3 -march = local -pthread => http://pastebin.com/T3yzViZP chương trình sắp xếp chính


1.621 giây! Tôi nghĩ bạn là người lãnh đạo, nhưng tôi nhanh chóng mất dấu với tất cả những câu trả lời này :)
static_rtti

2

Đây là một giải pháp tuần tự khác. Cái này sử dụng thực tế là các phần tử được phân phối bình thường và tôi nghĩ rằng ý tưởng thường được áp dụng để sắp xếp gần với thời gian tuyến tính.

Thuật toán là như thế này:

  • CDF gần đúng (xem phi()chức năng trong quá trình thực hiện)
  • Đối với tất cả các phần tử tính toán vị trí gần đúng trong mảng được sắp xếp: size * phi(x)
  • Đặt các phần tử trong một mảng mới gần với vị trí cuối cùng của chúng
    • Trong mảng đích thực hiện của tôi có một số khoảng trống trong đó vì vậy tôi không phải thay đổi quá nhiều phần tử khi chèn.
  • Sử dụng phần chèn để sắp xếp các phần tử cuối cùng (phần chèn là tuyến tính nếu khoảng cách đến vị trí cuối cùng nhỏ hơn hằng số).

Thật không may, hằng số ẩn là khá lớn và giải pháp này chậm gấp đôi so với thuật toán sắp xếp cơ số.


1
2.470s! Ý tưởng rất hay. Không có vấn đề gì khi giải pháp không nhanh nhất nếu các ý tưởng thú vị :)
static_rtti

1
Điều này giống như của tôi, nhưng nhóm các phép tính phi lại với nhau và dịch chuyển cùng nhau để có hiệu suất bộ đệm tốt hơn, phải không?
jonderry

@jonderry: Tôi đã nâng cao giải pháp của bạn, bây giờ tôi đã hiểu nó làm gì. Không có nghĩa là ăn cắp ý tưởng của bạn. Tôi đã bao gồm việc triển khai của bạn trong bộ thử nghiệm
Alexandru

2

Yêu thích cá nhân của tôi khi sử dụng Khối xây dựng theo luồng của Intel đã được đăng, nhưng đây là một giải pháp song song thô sơ sử dụng JDK 7 và API fork / tham gia mới của nó:

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

Tuyên bố từ chối trách nhiệm quan trọng : Tôi đã thực hiện phần thích ứng sắp xếp nhanh cho fork / tham gia từ: https://github.com/pmbauer/abul/tree/master/src/main/java/pmbauer/abul

Để chạy cái này, bạn cần một bản beta của JDK 7 (http://jdk7.java.net/doad.html).

Trên lõi tứ i7 (OS X) 2.93Ghz của tôi:

Tài liệu tham khảo Python

time python sort.py 50000000
sorting...

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

Java JDK 7 ngã ba / tham gia

time java ForkJoinQuicksortTask 50000000
Starting parallel computation

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

Tôi cũng đã thử thực hiện một số thử nghiệm với việc đọc song song và chuyển đổi các byte thành gấp đôi, nhưng tôi không thấy có sự khác biệt nào ở đó.

Cập nhật:

Nếu bất cứ ai muốn thử nghiệm tải dữ liệu song song, phiên bản tải song song bên dưới. Về lý thuyết, điều này có thể làm cho nó đi nhanh hơn một chút, nếu thiết bị IO của bạn có đủ dung lượng song song (SSD thường làm). Ngoài ra còn có một số chi phí trong việc tạo Nhân đôi từ byte, do đó cũng có khả năng đi song song nhanh hơn. Trên các hệ thống của tôi (SSD Ubuntu 10.10 / Nehalem Quad / Intel X25M và SSD OS X 10.6 / i7 Quad / Samsung) tôi không thấy bất kỳ sự khác biệt thực sự nào.

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

Cập nhật2:

Tôi đã thực thi mã trên một trong 12 máy dev lõi của chúng tôi với một sửa đổi nhỏ để đặt số lượng lõi cố định. Điều này đã cho kết quả như sau:

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

Trên hệ thống này, tôi cũng đã thử phiên bản Python mất 1m2.994 và phiên bản C ++ của Zjarek mất 1.925 (vì một số lý do, phiên bản C ++ của Zjarek dường như chạy tương đối nhanh hơn trên máy tính của static_rtti).

Tôi cũng đã thử những gì đã xảy ra nếu tôi tăng gấp đôi kích thước tệp lên gấp đôi 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

Trong trường hợp này, phiên bản C ++ của Zjarek mất 3.968. Python chỉ mất quá lâu ở đây.

Nhân đôi 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

Trong trường hợp này, phiên bản C ++ của Zjarek là 6.044s. Tôi thậm chí không thử Python.

Phiên bản C ++ rất phù hợp với kết quả của nó, trong đó Java dao động một chút. Đầu tiên, nó sẽ hiệu quả hơn một chút khi vấn đề trở nên lớn hơn, nhưng sau đó lại kém hiệu quả hơn.


1
Mã này không phân tích chính xác các giá trị kép cho tôi. Java 7 có bắt buộc phải phân tích chính xác các giá trị từ tệp không?
jonderry

1
A, ngớ ngẩn với tôi. Tôi đã quên thiết lập lại endian sau khi tôi tái cấu trúc mã IO cục bộ từ nhiều dòng thành một. Thông thường, Java 7 sẽ cần thiết, trừ khi bạn đã thêm fork / tham gia riêng vào Java 6.
arjan

3.411s trên máy của tôi. Không tệ, nhưng chậm hơn giải pháp java của koumes21 :)
static_rtti

1
Tôi sẽ thử giải pháp của koumes21 tại đây quá cục bộ để xem sự khác biệt tương đối trên hệ thống của tôi. Dù sao, không có gì xấu hổ khi 'mất' từ koumes21 vì đó là một giải pháp thông minh hơn nhiều. Đây chỉ là một loại sắp xếp nhanh gần như tiêu chuẩn được ném vào một ngã ba / nhóm tham gia;)
arjan

1

Một phiên bản sử dụng pthreads truyền thống. Mã hợp nhất được sao chép từ câu trả lời của Guvante. Biên dịch với 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;
}  

Trên máy tính xách tay của tôi, tôi nhận được kết quả như sau:

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

1

Dưới đây là một triển khai C99 tuần tự cố gắng thực sự sử dụng phân phối đã biết. Về cơ bản, nó thực hiện một vòng phân loại xô bằng cách sử dụng thông tin phân phối, sau đó một vài vòng quicksort trên mỗi nhóm giả định phân phối đồng đều trong giới hạn của nhóm và cuối cùng là sắp xếp lựa chọn đã sửa đổi để sao chép dữ liệu trở lại bộ đệm ban đầu. Quicksort ghi nhớ các điểm phân chia, vì vậy sắp xếp lựa chọn chỉ cần hoạt động trên các khung nhỏ. Và mặc dù (vì?) Của tất cả sự phức tạp đó, nó thậm chí không thực sự nhanh.

Để đánh giá nhanh, các giá trị được lấy mẫu ở một vài điểm và sau đó chỉ sử dụng phép nội suy tuyến tính. Nó thực sự không quan trọng nếu được đánh giá chính xác, miễn là phép tính gần đúng là đơn điệu.

Kích thước thùng được chọn sao cho khả năng tràn thùng là không đáng kể. Chính xác hơn, với các tham số hiện tại, khả năng bộ dữ liệu gồm 50000000 phần tử sẽ gây ra tràn bin là 3,65e-09. (Điều này có thể được tính bằng cách sử dụng chức năng sống sót của phân phối Poisson .)

Để biên dịch, vui lòng sử dụng

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

Vì có nhiều tính toán hơn đáng kể so với các giải pháp khác, các cờ trình biên dịch này là cần thiết để làm cho nó ít nhất là nhanh chóng hợp lý. Không có -msse3chuyển đổi từ doubleđể inttrở nên thực sự chậm. Nếu kiến ​​trúc của bạn không hỗ trợ SSE3, những chuyển đổi này cũng có thể được thực hiện bằng cách sử dụng lrint()chức năng.

Mã này khá xấu - không chắc điều này có đáp ứng yêu cầu "có thể đọc được" hay không ...

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

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

double phi_vals[PHI_VALS];

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

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

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

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

4.098s! Tôi đã phải thêm -lm để biên dịch nó (cho erf).
static_rtti

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

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

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

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

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


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


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


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

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

        return 0;
}

Điều này sử dụng erf () để đặt từng phần tử một cách thích hợp vào một thùng, sau đó sắp xếp từng thùng. Nó giữ cho các mảng hoàn toàn tại chỗ.

Vượt qua đầu tiên: docallel () đếm số lượng phần tử trong mỗi thùng.

Pass thứ hai: phân vùng () hoán vị mảng, đặt từng phần tử vào thùng thích hợp của nó

Vượt qua thứ ba: sortbins () thực hiện một qsort trên mỗi thùng.

Đó là loại ngây thơ và gọi hàm erf () đắt tiền hai lần cho mỗi giá trị. Các đường chuyền đầu tiên và thứ ba có khả năng song song. Thứ hai là rất nối tiếp và có thể bị chậm lại bởi các mẫu truy cập bộ nhớ rất ngẫu nhiên của nó. Cũng có thể đáng để lưu trữ số lượng thùng của mỗi đôi, tùy thuộc vào tỷ lệ tốc độ bộ nhớ của CPU.

Chương trình này cho phép bạn chọn số lượng thùng để sử dụng. Chỉ cần thêm một số thứ hai vào dòng lệnh. Tôi đã biên dịch nó với gcc -O3, nhưng máy của tôi rất yếu Tôi không thể cho bạn biết bất kỳ số hiệu suất tốt nào.

Chỉnh sửa: Gặp sự cố! Chương trình C của tôi đã biến đổi một cách kỳ diệu thành chương trình C ++ bằng cách sử dụng std :: sort!


Bạn có thể sử dụng phi cho một stdn normal_cdf nhanh hơn.
Alexandru

Tôi nên đặt bao nhiêu thùng, khoảng?
static_rtti

@Alexandru: Tôi đã thêm một xấp xỉ tuyến tính piecewise vào Normcdf và chỉ đạt được khoảng 5% tốc độ.
frud

@static_rtti: Bạn không cần phải đặt bất kỳ. Theo mặc định, mã chọn số lượng thùng để kích thước thùng trung bình là 10/11 là 128kb. Quá ít thùng và bạn không nhận được lợi ích của việc phân vùng. Quá nhiều và giai đoạn phân vùng sa lầy do tràn bộ đệm.
frud

10,6 giây! Tôi đã thử chơi một chút với số thùng và tôi nhận được kết quả tốt nhất với 5000 (hơi vượt quá giá trị mặc định là 3356). Tôi phải nói rằng tôi đã dự kiến ​​sẽ thấy một hiệu suất tốt hơn cho giải pháp của bạn ... Có lẽ thực tế là bạn đang sử dụng qsort thay vì std :: loại giải pháp C ++ nhanh hơn?
static_rtti

1

Hãy xem triển khai sắp xếp radix của Michael Herf ( Radix Tricks ). Trên máy của tôi sắp xếp nhanh hơn 5 lần so với std::sortthuật toán trong câu trả lời đầu tiên của tôi. Tên của chức năng sắp xếp là 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;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.