การประกวด: วิธีที่เร็วที่สุดในการจัดเรียงข้อมูลแบบกระจายขนาดใหญ่ของเกาส์เซียน


71

ตามความสนใจในคำถามนี้ฉันคิดว่ามันน่าสนใจที่จะตอบคำถามให้มีวัตถุประสงค์และเชิงปริมาณมากขึ้นโดยเสนอการประกวด

ความคิดนั้นง่าย: ฉันได้สร้างไฟล์ไบนารีที่มี 50 ล้าน gaussian- กระจายคู่ (เฉลี่ย: 0, stdev 1) เป้าหมายคือการสร้างโปรแกรมที่จะจัดเรียงสิ่งเหล่านี้ในหน่วยความจำโดยเร็วที่สุด การดำเนินการอ้างอิงที่ง่ายมากใน python ใช้เวลา 1m4s ในการดำเนินการให้เสร็จสมบูรณ์ เราไปได้ต่ำแค่ไหน?

กฎมีดังนี้คำตอบด้วยโปรแกรมที่เปิดไฟล์ "gaussian.dat" และเรียงลำดับตัวเลขในหน่วยความจำ (ไม่จำเป็นต้องส่งออก) และคำแนะนำสำหรับการสร้างและเรียกใช้โปรแกรม โปรแกรมจะต้องสามารถทำงานกับเครื่อง Arch Linux ของฉันได้ (หมายถึงคุณสามารถใช้ภาษาโปรแกรมหรือไลบรารีใด ๆ ที่ติดตั้งได้ง่ายในระบบนี้)

โปรแกรมต้องสามารถอ่านได้อย่างมีเหตุผลเพื่อให้ฉันมั่นใจได้ว่าจะปลอดภัยในการเปิดตัว (ไม่มีวิธีแก้ปัญหาเฉพาะแอสเซมเบลอร์เท่านั้นโปรด!)

ฉันจะรันคำตอบบนเครื่องของฉัน (quad core, 4 Gigabytes of 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)

แก้ไข: RAM เพียง 4 GB ขออภัย

แก้ไข # 2: โปรดทราบว่าจุดของการประกวดคือการดูว่าเราสามารถใช้ข้อมูลก่อนเกี่ยวกับข้อมูล ไม่ควรที่จะเป็นการจับคู่ระหว่างการใช้ภาษาโปรแกรมที่แตกต่างกัน!


1
ใช้แต่ละค่าและย้ายไปยังตำแหน่ง "ที่คาดหวัง" โดยตรงสำหรับค่าที่แทนที่ ไม่แน่ใจว่าจะแก้ไขปัญหาสองสามข้อได้อย่างไร เมื่อเสร็จแล้วให้เรียงลำดับฟองจนกว่าจะเสร็จสมบูรณ์ (คู่ผ่านควรทำ)

1
ฉันจะโพสต์วิธีแก้ปัญหาการจัดเรียงในวันพรุ่งนี้ถ้าตอนนี้ยังไม่ปิด :) :)

1
@static_rtti - ในฐานะผู้ใช้ CG ที่หนักหน่วงนี่คือสิ่งที่ "เรา" ชอบที่จะแฮ็ค CG ที่ SE สำหรับ mods การอ่านใด ๆ ให้ย้ายสิ่งนี้ไปที่ CG อย่าปิดมัน
arrdem

1
ยินดีต้อนรับสู่ CodeGolf.SE! ฉันได้ล้างความเห็นจำนวนมากจากต้นฉบับ SO เกี่ยวกับสิ่งที่ทำหรือไม่ได้อยู่ในนั้นและติดแท็กอีกครั้งเพื่อให้ใกล้เคียงกับหลัก CodeGolf.SE
dmckee

2
ปัญหาที่ยุ่งยากอย่างหนึ่งที่นี่คือเรามองหาเกณฑ์การชนะอย่างมีวัตถุประสงค์และ "เร็วที่สุด" แนะนำการพึ่งพาแพลตฟอร์ม ... อัลกอริทึม O (n ^ {1.2}) นำไปใช้กับเครื่องเสมือน cpython เอาชนะ O (n ^ {1.3} ) อัลกอริทึมที่มีค่าคงที่ที่คล้ายกันนำมาใช้ใน c? ฉันมักจะแนะนำการอภิปรายบางอย่างเกี่ยวกับลักษณะการทำงานของแต่ละวิธีแก้ปัญหาเนื่องจากอาจช่วยให้ผู้ใช้ตัดสินว่าเกิดอะไรขึ้น
dmckee

คำตอบ:


13

นี่คือวิธีการแก้ปัญหาใน C ++ ซึ่งแบ่งพาร์ติชันเป็นตัวเลขลงในที่เก็บที่มีจำนวนองค์ประกอบเท่ากันจากนั้นจึงจัดเรียงที่เก็บข้อมูลแยกกัน จะคำนวณตารางล่วงหน้าของฟังก์ชันการแจกแจงสะสมตามสูตรบางอย่างจาก Wikipedia แล้วแทรกค่าจากตารางนี้เพื่อให้ได้ค่าประมาณที่รวดเร็ว

หลายขั้นตอนรันในหลายเธรดเพื่อใช้ประโยชน์จากสี่คอร์

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

#include <tbb/parallel_for.h>

using namespace std;

typedef unsigned long long ull;

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

const double fourOverPI = 4 / M_PI;

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

const double sqrt2 = sqrt(2);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    puts("done.");

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

    //print(data, N);

    return 0;
}

หากต้องการคอมไพล์และรันให้ใช้คำสั่งนี้:

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

แก้ไข:ถังทั้งหมดจะถูกวางลงในอาร์เรย์เดียวกันเพื่อลบความจำเป็นในการคัดลอกถังกลับเข้าไปในอาร์เรย์ นอกจากนี้ขนาดของตารางที่มีค่าที่คำนวณล่วงหน้าได้ลดลงเนื่องจากค่ามีความถูกต้องเพียงพอ แต่ถึงกระนั้นถ้าฉันเปลี่ยนจำนวนของถังที่สูงกว่า 256 โปรแกรมจะใช้เวลานานกว่าเมื่อใช้จำนวนถังนั้น

แก้ไข:อัลกอริทึมเดียวกันภาษาการเขียนโปรแกรมที่แตกต่างกัน ฉันใช้ C ++ แทน Java และเวลาทำงานลดลงจาก ~ 3.2s เป็น ~ 2.35s บนเครื่องของฉัน จำนวนถังที่เหมาะสมยังคงอยู่ที่ประมาณ 256 (บนคอมพิวเตอร์ของฉันอีกครั้ง)

tbb ยอดเยี่ยมจริงๆ

แก้ไข:ฉันได้รับแรงบันดาลใจจากทางออกที่ยอดเยี่ยมของ Alexandru และแทนที่ std :: sort ในช่วงสุดท้ายด้วย Radix sort ที่เขาแก้ไข ฉันใช้วิธีที่แตกต่างเพื่อจัดการกับจำนวนบวก / ลบแม้ว่ามันจะต้องผ่านจำนวนมากขึ้น ฉันยังตัดสินใจที่จะเรียงลำดับแถวอย่างแน่นอนและลบเรียงลำดับแทรก ฉันจะใช้เวลาทดสอบในภายหลังว่าการเปลี่ยนแปลงเหล่านี้มีผลต่อประสิทธิภาพการทำงานอย่างไรและอาจเปลี่ยนกลับได้ อย่างไรก็ตามโดยใช้การเรียงลำดับ radix เวลาลดลงจาก ~ 2.35s เป็น ~ 1.63s


ดี ฉันได้ 3.055 แล้ว ต่ำสุดที่ฉันสามารถได้รับของฉันคือ 6.3 ฉันกำลังเลือกของคุณเพื่อให้ได้สถิติที่ดีขึ้น ทำไมคุณถึงเลือก 256 เป็นจำนวนถัง? ฉันลอง 128 และ 512 แต่ 256 ทำงานได้ดีที่สุด
สกอตต์

ทำไมฉันถึงเลือก 256 เป็นจำนวนถัง? ฉันลอง 128 และ 512 แต่ 256 ทำงานได้ดีที่สุด :) ฉันพบว่าสังเกตุและฉันไม่แน่ใจว่าทำไมการเพิ่มจำนวนของที่เก็บข้อมูลทำให้อัลกอริทึมช้าลง - การจัดสรรหน่วยความจำไม่ควรใช้เวลานานขนาดนั้น อาจมีบางอย่างที่เกี่ยวข้องกับขนาดแคช?
k21

2.725s บนเครื่องของฉัน ค่อนข้างดีสำหรับโซลูชัน java โดยคำนึงถึงเวลาในการโหลดของ JVM
static_rtti

2
ฉันสลับรหัสของคุณไปใช้แพ็คเกจ nio ต่อโซลูชันของฉันและ Arjan (ใช้ไวยากรณ์ของเขาเพราะสะอาดกว่าของฉัน) และสามารถรับได้เร็วขึ้น 0.3 วินาที ฉันได้รับ ssd ฉันสงสัยว่าสิ่งที่เกี่ยวข้องอาจเป็นเช่นนั้น นอกจากนี้ยังกำจัดบางส่วนของ twiddling ของคุณ ส่วนดัดแปลงอยู่ที่นี่
สกอตต์

3
นี่เป็นวิธีแก้ปัญหาขนานที่เร็วที่สุดในการทดสอบของฉัน (16core cpu) 1.22 ห่างจากสถานที่ที่สอง 1.94
Alexandru

13

โดยไม่ต้องฉลาดพอที่จะจัดหาเครื่องคัดเเยกที่เร็วขึ้นนี่คือตัว C ที่ควรจะเทียบเท่ากับ Python ของคุณ:

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

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

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

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

คอมไพล์ด้วยgcc -O3บนเครื่องของฉันใช้เวลาน้อยกว่า Python มากกว่านาที: ประมาณ 11 วินาทีเทียบกับ 87 วินาที


1
ใช้ 10.086s บนเครื่องของฉันซึ่งทำให้คุณเป็นผู้นำคนปัจจุบัน! แต่ผมค่อนข้างมั่นใจว่าเราสามารถทำได้ดี :)

1
คุณสามารถลองลบผู้ประกอบการที่สามและเพียงแค่คืนค่า 1 สำหรับกรณีนั้นได้เนื่องจากการสุ่มคู่นั้นไม่เหมือนกันในจำนวนข้อมูลเหล่านี้
Codism

@Codism: ฉันจะเพิ่มที่เราไม่สนใจเกี่ยวกับการแลกเปลี่ยนตำแหน่งของข้อมูลที่เทียบเท่าดังนั้นดังนั้นแม้ว่าเราจะได้รับค่าที่เท่าเทียมกันมันจะเป็นความเรียบง่ายที่เหมาะสม

10

ฉันแบ่งพาร์ติชันออกเป็นส่วน ๆ ตามค่าเบี่ยงเบนมาตรฐานที่ควรแบ่งเป็น 4 ส่วน แก้ไข: เขียนซ้ำไปยังพาร์ติชันตามค่า x ในhttp://en.wikipedia.org/wiki/Error_function#Table_of_values

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

ฉันลองใช้ถังขนาดเล็ก แต่ดูเหมือนว่าจะมีผลเพียงเล็กน้อยเมื่อ 2 * เกินจำนวนแกนที่มีอยู่ หากไม่มีคอลเลกชันแบบขนานใด ๆ ฉันจะใช้เวลา 37 วินาทีในกล่องของฉันและ 24 กับคอลเล็กชันแบบขนาน หากการแบ่งพาร์ติชันด้วยการแจกจ่ายคุณไม่สามารถใช้อาร์เรย์ได้ดังนั้นจึงมีค่าใช้จ่ายเพิ่มเติม ฉันไม่ชัดเจนว่าเมื่อใดค่าจะถูกจัดวาง / ไม่ได้บรรจุในสกาล่า

ฉันใช้สกาล่า 2.9 สำหรับการสะสมแบบขนาน คุณสามารถดาวน์โหลด tar.gz ได้

เพื่อคอมไพล์: scalac SortFile.scala (ฉันเพิ่งคัดลอกโดยตรงในโฟลเดอร์ scala / bin

วิธีเรียกใช้: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (ฉันวิ่งด้วย ram 2 กิกะไบต์และได้ในเวลาเดียวกัน)

แก้ไข: ลบ allocateDirect ช้ากว่าการจัดสรร ลบการกำหนดขนาดเริ่มต้นสำหรับบัฟเฟอร์อาร์เรย์ ที่จริงแล้วทำให้มันอ่านค่าทั้งหมด 50,000,000 เขียนซ้ำเพื่อหวังว่าจะหลีกเลี่ยงปัญหาการล็อกอัตโนมัติ (ยังช้ากว่าไร้เดียงสา 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! ดีสำหรับโซลูชันแบบสกาล่าฉันเดาว่า ... นอกจากนี้ไชโยสำหรับการแก้ปัญหาแรกที่ใช้การกระจายแบบเกาส์ในทางใดทางหนึ่ง!

1
ฉันตั้งเป้าที่จะแข่งขันกับโซลูชัน c # เท่านั้น ไม่คิดว่าฉันจะชนะ c / c ++ นอกจากนี้ .. มันมีพฤติกรรมที่แตกต่างกันมากสำหรับคุณกว่าสำหรับฉัน ฉันใช้ openJDK ในตอนท้ายและช้ากว่ามาก ฉันสงสัยว่าการเพิ่มพาร์ทิชันเพิ่มเติมจะช่วยใน env ของคุณ
สกอตต์

9

เพียงแค่ใส่มันในไฟล์ cs และคอมไพล์ด้วย csc ในทางทฤษฎี: (ต้องการ mono)

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

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

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

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

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

ฉันสามารถใช้งานโซลูชันของคุณกับ Mono ได้หรือไม่? ฉันควรทำอย่างไร

ยังไม่ได้ใช้โมโนไม่คิดอย่างนั้นคุณควรรวบรวม F # จากนั้นเรียกใช้

1
อัปเดตเพื่อใช้สี่เธรดเพื่อปรับปรุงประสิทธิภาพ ตอนนี้ให้ฉัน 6 วินาที โปรดทราบว่าสิ่งนี้สามารถปรับปรุงได้อย่างมีนัยสำคัญ (น่าจะเป็น 5 วินาที) หากคุณใช้อาเรย์สำรองเพียงอันเดียวและหลีกเลี่ยงการเริ่มต้นหน่วยความจำจำนวนมากเป็นศูนย์ซึ่งทำโดย CLR เนื่องจากทุกอย่างจะถูกเขียนลงอย่างน้อยหนึ่งครั้ง

1
9.598s บนเครื่องของฉัน! คุณเป็นผู้นำคนปัจจุบัน :)

1
แม่ของฉันบอกให้ฉันอยู่ห่างจากพวกโมโนด้วย!

8

เนื่องจากคุณรู้ว่าการกระจายคืออะไรคุณสามารถใช้การเรียงลำดับการจัดทำดัชนี O (N) ได้โดยตรง (ถ้าคุณสงสัยว่ามันคืออะไรสมมติว่าคุณมีไพ่ 52 ใบและคุณต้องการเรียงลำดับเพียงมีถังขยะ 52 ใบและโยนการ์ดแต่ละใบลงในถังขยะของตัวเอง)

คุณมี 5e7 doubles จัดสรรอาร์เรย์ผลลัพธ์ R ของ 5e7 เป็นสองเท่า ใช้เวลาแต่ละหมายเลขและได้รับx โดยทั่วไปทำi = phi(x) * 5e7 R[i] = xมีวิธีจัดการกับการชนเช่นการย้ายหมายเลขที่อาจชนกับ (เช่นในการเข้ารหัสแฮชธรรมดา) หรือมิฉะนั้นคุณสามารถทำให้ R มีขนาดใหญ่ขึ้นสองสามครั้งเต็มไปด้วยค่าว่างที่ไม่ซ้ำ ในตอนท้ายคุณแค่กวาดองค์ประกอบของอาร์

phiเป็นเพียงฟังก์ชันการแจกแจงสะสมแบบเกาส์เซียน มันจะแปลงตัวเลขการแจกแจงแบบเกาส์ระหว่าง +/- อินฟินิตี้เป็นตัวเลขการแจกแจงแบบสม่ำเสมอระหว่าง 0 ถึง 1 วิธีง่ายๆในการคำนวณมันคือการค้นหาตารางและการแก้ไข


3
ระวัง: คุณรู้ว่าการกระจายตัวโดยประมาณไม่ใช่การกระจายตัวที่แน่นอน คุณรู้ว่าข้อมูลถูกสร้างขึ้นโดยใช้กฎหมายแบบเกาส์เซียน แต่เนื่องจากมีขอบเขต จำกัด จึงไม่เป็นไปตามแบบเกาส์เซียนอย่างแน่นอน

@static_rtti: ในกรณีนี้การประมาณค่าที่จำเป็นของ phi จะสร้างความยุ่งยากที่ใหญ่กว่าความผิดปกติใด ๆ ในชุดข้อมูล IMO

1
@static_rtti: ไม่จำเป็นต้องแน่นอน มันต้องกระจายข้อมูลออกไปเท่านั้นดังนั้นมันจึงเป็นรูปแบบโดยประมาณดังนั้นจึงไม่ได้รวมตัวกันมากเกินไปในบางที่

สมมติว่าคุณมี 5e7 doubles ทำไมไม่ทำให้แต่ละรายการใน R เป็นเวกเตอร์ของ, พูด, 5e6 เวกเตอร์ของสองเท่า จากนั้น push_back แต่ละคู่ในเวกเตอร์ที่เหมาะสม จัดเรียงเวกเตอร์และคุณทำเสร็จแล้ว นี้ควรใช้เวลาเชิงเส้นในขนาดของอินพุต
Neil G

ที่จริงแล้วฉันเห็นว่า mdkess เกิดขึ้นพร้อมกับโซลูชันนั้น
Neil G

8

นี่คือวิธีแก้ปัญหาต่อเนื่องอื่น:

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

typedef unsigned long long ull;

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

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

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

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

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

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

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

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

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

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

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

ฉันสงสัยว่ามันจะเป็นวิธีการแก้ปัญหาแบบมัลติเธรด แต่การกำหนดเวลาในแล็ปท็อป i7 ของฉันคือ (stdsort เป็นโซลูชัน C ++ ที่มีให้ในคำตอบอื่น):

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

โปรดทราบว่าโซลูชันนี้มีความซับซ้อนของเวลาเชิงเส้น (เพราะใช้การแทนแบบพิเศษของคู่)

แก้ไข : แก้ไขลำดับขององค์ประกอบที่จะเพิ่มขึ้น

แก้ไข : ปรับปรุงความเร็วเกือบครึ่งวินาที

แก้ไข : ปรับปรุงความเร็วอีก 0.7 วินาที ทำให้อัลกอริทึมเป็นมิตรกับแคชมากขึ้น

แก้ไข : ปรับปรุงความเร็วอีก 1 วินาที เนื่องจากมีองค์ประกอบเพียง 50,000,000 เท่านั้นฉันจึงสามารถจัดเรียง mantissa และใช้ sort sort (ซึ่งเป็นมิตรกับแคช) เพื่อแก้ไของค์ประกอบนอกสถานที่ แนวคิดนี้ลบรอบการวนซ้ำสองรอบออกจากลูปการเรียงลำดับ radix ล่าสุด

แก้ไข : 0.16 วินาทีน้อยลง First std :: reverse สามารถกำจัดได้ถ้าลำดับการเรียงกลับด้าน


ตอนนี้กำลังได้รับความสนใจ! อัลกอริทึมการเรียงลำดับมันคืออะไร?
static_rtti

2
อย่างน้อยหลักที่สำคัญการจัดเรียง Radix คุณสามารถจัดเรียง mantissa จากนั้นเป็นเลขชี้กำลังแล้วตามด้วยเครื่องหมาย อัลกอริทึมที่นำเสนอที่นี่จะใช้ความคิดนี้อีกขั้นหนึ่ง สามารถทำการขนานโดยใช้แนวคิดการแบ่งพาร์ติชันที่ให้ไว้ในคำตอบที่ต่างออกไป
Alexandru

ค่อนข้างเร็วสำหรับโซลูชันเธรดเดียว: 2.552 วินาที! คุณคิดว่าคุณสามารถเปลี่ยนวิธีแก้ปัญหาของคุณเพื่อใช้ประโยชน์จากข้อเท็จจริงที่ว่าข้อมูลถูกแจกจ่ายตามปกติหรือไม่? คุณสามารถทำได้ดีกว่าโซลูชั่นแบบมัลติเธรดที่ดีที่สุดในปัจจุบัน
static_rtti

1
@static_rtti: ฉันเห็นว่า Damascus Steel ได้โพสต์เวอร์ชันของการใช้งานแบบมัลติเธรดแล้ว ฉันปรับปรุงพฤติกรรมการแคชของอัลกอริทึมนี้ดังนั้นคุณควรเพิ่มเวลาที่ดีขึ้นในตอนนี้ โปรดทดสอบเวอร์ชันใหม่นี้
Alexandru

2
1.459s ในการทดสอบ latests ของฉัน ในขณะที่ทางออกนี้ไม่ใช่ผู้ชนะตามกฎของฉันมันสมควรได้รับความชื่นชมอย่างมาก ขอแสดงความยินดี!
static_rtti

6

ใช้วิธีการแก้ปัญหาของ Christian Ammer และต่อขนานกับหน่วยการสร้างเธรดของ Intel

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

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

หากคุณมีสิทธิ์เข้าถึงไลบรารี Performance Primitives (IPP) ของ Intel คุณสามารถใช้การเรียงลำดับแบบ Radix เพียงแค่แทนที่

#include <tbb/parallel_sort.h>

กับ

#include "ipps.h"

และ

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

กับ

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

บนแล็ปท็อปดูอัลคอร์ของฉัน

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

1
2.958s! TBB ดูเท่ห์และใช้งานง่าย!

2
TBB นั้นยอดเยี่ยมอย่างน่าหัวเราะ เป็นระดับที่เหมาะสมของนามธรรมสำหรับงานอัลกอริทึม
drxzcl

5

วิธีการเกี่ยวกับการใช้งานของquicksort แบบขนานที่เลือกค่า pivot ตามสถิติของการแจกจ่ายดังนั้นจึงมั่นใจได้ว่าพาร์ติชันที่มีขนาดเท่ากัน? เดือยแรกจะอยู่ที่ค่าเฉลี่ย (ศูนย์ในกรณีนี้) คู่ต่อไปจะอยู่ที่เปอร์เซนต์ไทล์ 25 และ 75 (+/- -0.67449 ค่าเบี่ยงเบนมาตรฐาน) และอื่น ๆ โดยแต่ละพาร์ติชันจะลดชุดข้อมูลที่เหลือลงครึ่งหนึ่งหรือมากกว่า สมบูรณ์แบบน้อยลง


นั่นเป็นสิ่งที่ฉันทำอย่างแน่นอนว่าคุณได้โพสต์นี้ก่อนที่ฉันจะสามารถเขียนเสร็จได้

5

น่าเกลียดมาก (ทำไมต้องใช้อาร์เรย์เมื่อฉันสามารถใช้ตัวแปรที่ลงท้ายด้วยตัวเลข) แต่รหัสรวดเร็ว (พยายามครั้งแรกที่จะ std :: กระทู้) เวลาทั้งหมด (เวลาจริง) ในระบบของฉัน 1,8 s (เปรียบเทียบกับ std :: sort () 4,8 s) คอมไพล์ด้วย g ++ -std = c ++ 0x -O3 -march = native -pthread เพียงแค่ส่งข้อมูลผ่าน stdin (ใช้งานได้ 50M เท่านั้น)

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

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

// แก้ไขเปลี่ยนแปลงเพื่ออ่านไฟล์ gaussian.dat


คุณสามารถเปลี่ยนเป็นอ่าน gaussian.dat ได้ตามวิธีการแก้ปัญหา C ++ ข้างต้นหรือไม่

ฉันจะลองอีกครั้งเมื่อฉันกลับบ้าน
static_rtti

ทางออกที่ดีมากคุณเป็นผู้นำคนปัจจุบัน (1.949s)! และการใช้งานที่ดีของการกระจาย Gaussian :)
static_rtti

4

โซลูชัน C ++ ที่ใช้std::sort(ในที่สุดเร็วกว่า qsort เกี่ยวกับประสิทธิภาพของ 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 ที่ระบุฉันสามารถสร้างgaussian.datไฟล์ที่มีขนาด 25mio เพียงสองเท่าเท่านั้น (โดยไม่ได้รับข้อผิดพลาดของหน่วยความจำ) แต่ฉันสนใจมากว่า std :: sort algorithm ทำงานนานแค่ไหน


6.425s! ตามที่คาดไว้ C ++ ส่องแสง :)

@static_rtti: ฉันได้ลองอัลกอริทึมTimsort Swensons (ตามที่แนะนำจาก Matthieu M. ในคำถามแรกของคุณ) ฉันต้องทำการเปลี่ยนแปลงsort.hไฟล์เพื่อคอมไพล์ด้วย C ++ std::sortมันเป็นเรื่องของสองเท่าช้ากว่า ไม่ทราบสาเหตุอาจเป็นเพราะการปรับแต่งคอมไพเลอร์?
Christian Ammer

4

นี่คือส่วนผสมของการเรียงลำดับเลขฐานสองของ Alexandru กับการหมุนรอบตัวอัจฉริยะของ Zjarek รวบรวมมันด้วย

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

คุณสามารถเปลี่ยนขนาด Radix ได้โดยกำหนด STEP (เช่นเพิ่ม -DSTEP = 11) ฉันพบสิ่งที่ดีที่สุดสำหรับแล็ปท็อปของฉันคือ 8 (ค่าเริ่มต้น)

โดยค่าเริ่มต้นมันแยกปัญหาออกเป็น 4 ชิ้นและเรียกใช้ในหลายเธรด คุณสามารถเปลี่ยนได้โดยส่งพารามิเตอร์ความลึกไปยังบรรทัดคำสั่ง ดังนั้นถ้าคุณมีสองคอร์ให้เรียกใช้เป็น

sorter_gaussian_radix 50000000 1

และถ้าคุณมี 16 แกน

sorter_gaussian_radix 50000000 4

ความลึกสูงสุดในขณะนี้คือ 6 (64 เธรด) หากคุณใส่หลายระดับเกินไปคุณจะช้าลงรหัส

สิ่งหนึ่งที่ฉันลองก็คือ Radix เรียงจากไลบรารี Intel Performance Primitives (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% ในเครื่องของฉัน

แก้ไข : นี่เป็นการดำเนินการแบบเรียกซ้ำดังนั้นจึงควรทำงานได้ดีกับเครื่องหลัก 16 เครื่องของ Alexandru นอกจากนี้ยังใช้การปรับปรุงล่าสุดของ Alexandru และลบหนึ่งในสิ่งที่ตรงกันข้าม สำหรับฉันสิ่งนี้ให้การปรับปรุง 20%

แก้ไข : แก้ไขข้อผิดพลาดสัญญาณที่ทำให้ไม่มีประสิทธิภาพเมื่อมีมากกว่า 2 แกน

แก้ไข : ลบแลมบ์ดาดังนั้นมันจะรวบรวมกับ gcc รุ่นเก่ากว่า ซึ่งรวมถึงรูปแบบรหัส IPP ที่แสดงความคิดเห็น ฉันยังแก้ไขเอกสารสำหรับการทำงานบน 16 คอร์ เท่าที่ฉันจะบอกได้นี่คือการดำเนินการที่เร็วที่สุด

แก้ไข : แก้ไขข้อผิดพลาดเมื่อ STEP ไม่ใช่ 8 เพิ่มจำนวนเธรดสูงสุดเป็น 64 เพิ่มข้อมูลช่วงเวลา


ดี การเรียงลำดับ Radix เป็นแคชที่ไม่เป็นมิตรมาก ดูว่าคุณจะได้รับผลลัพธ์ที่ดีขึ้นโดยการเปลี่ยนstep(11 ดีที่สุดในแล็ปท็อปของฉัน)
Alexandru

คุณมีข้อผิดพลาด: ควรจะเป็นint cnt[mask] เพื่อให้ได้ผลลัพธ์ที่ดีกว่าใช้ค่าคงที่int cnt[mask + 1] int cnt[1 << 16]
Alexandru

ฉันจะลองวิธีแก้ปัญหาทั้งหมดนี้ในวันนี้เมื่อฉันกลับถึงบ้าน
static_rtti

1.534s !!! ฉันคิดว่าเรามีผู้นำ :-D
static_rtti

@static_rtti: คุณลองอีกครั้งได้ไหม มันเร็วขึ้นกว่าครั้งที่แล้วอย่างมาก บนเครื่องของฉันมันเร็วกว่าโซลูชันอื่น ๆ อย่างมาก
เหล็กดามัสกัส

2

ฉันเดาว่าสิ่งนี้ขึ้นอยู่กับสิ่งที่คุณต้องการจะทำ หากคุณต้องการเรียงลำดับ Gaussians แล้วสิ่งนี้จะไม่ช่วยคุณ แต่ถ้าคุณต้องการเรียง Gaussians เรียงนี้จะ แม้ว่านี่จะพลาดปัญหาไปนิดหน่อย แต่ฉันคิดว่ามันน่าสนใจที่จะเปรียบเทียบกับรูทีนการเรียงลำดับตามจริง

หากคุณต้องการบางสิ่งบางอย่างที่รวดเร็วให้ทำน้อยลง

แทนที่จะสร้างกลุ่มตัวอย่างสุ่มจากการแจกแจงแบบปกติแล้วเรียงลำดับคุณสามารถสร้างกลุ่มตัวอย่างจากการแจกแจงแบบปกติตามลำดับที่เรียงลำดับ

คุณสามารถใช้วิธีการแก้ปัญหาที่นี่เพื่อสร้างตัวเลขสุ่มที่เหมือนกันในการเรียงลำดับ จากนั้นคุณสามารถใช้ CDF ผกผัน (scipy.stats.norm.ppf) ของการแจกแจงแบบปกติที่จะเปิดตัวเลขสุ่มเครื่องแบบเป็นตัวเลขจากการกระจายปกติผ่านทางผกผันเปลี่ยนการสุ่มตัวอย่าง

import scipy.stats
import random

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

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

หากคุณต้องการให้มือของคุณสกปรกยิ่งขึ้นฉันเดาว่าคุณอาจสามารถเพิ่มความเร็วในการคำนวณ c ผกผันจำนวนมากได้โดยใช้วิธีการวนซ้ำบางอย่างและใช้ผลลัพธ์ก่อนหน้านี้เป็นเดาเริ่มต้นของคุณ เนื่องจากการเดาใกล้กันมากการทำซ้ำครั้งเดียวอาจให้ความแม่นยำที่ยอดเยี่ยม


2
คำตอบที่ดี แต่นั่นเป็นการโกง :) แนวคิดของคำถามของฉันคือในขณะที่อัลกอริทึมการเรียงลำดับได้รับความสนใจอย่างมากแทบจะไม่มีวรรณกรรมเกี่ยวกับการใช้ความรู้เดิมเกี่ยวกับข้อมูลสำหรับการเรียงลำดับถึงแม้ว่าเอกสารไม่กี่ฉบับที่มี แก้ไขปัญหาได้รายงานว่าได้รับผลกำไรที่ดี ดังนั้นเรามาดูสิ่งที่เป็นไปได้!

2

ลองวิธีแก้ปัญหาของ Guvante ที่เปลี่ยนไปด้วย Main () นี้มันเริ่มเรียงลำดับทันทีที่อ่าน 1/4 IO เสร็จเร็วขึ้นในการทดสอบของฉัน:

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

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

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

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

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

8.933s เร็วขึ้นเล็กน้อย :)

2

เมื่อคุณรู้ว่าการกระจายความคิดของฉันคือการสร้าง k buckets แต่ละอันมีจำนวนองค์ประกอบที่คาดหวังเท่ากัน (เนื่องจากคุณรู้ว่าการกระจายคุณสามารถคำนวณได้) จากนั้นในเวลา O (n) กวาดอาร์เรย์และใส่องค์ประกอบลงในที่เก็บข้อมูล

จากนั้นจัดเรียงถังพร้อมกัน สมมติว่าคุณมี k buckets และองค์ประกอบ n ที่ฝากข้อมูลจะใช้เวลา (n / k) lg (n / k) เพื่อเรียงลำดับ ตอนนี้สมมติว่าคุณมีโปรเซสเซอร์ p ที่คุณสามารถใช้ได้ เนื่องจากถังสามารถจัดเรียงอย่างอิสระคุณมีตัวคูณของเพดาน (k / p) ที่จะจัดการกับ สิ่งนี้จะให้รันไทม์สุดท้ายของ n + ceil (k / p) * (n / k) lg (n / k) ซึ่งควรจะเร็วกว่า n lg n หากคุณเลือก k well


ฉันคิดว่านี่เป็นทางออกที่ดีที่สุด
Neil G

คุณไม่ทราบจำนวนองค์ประกอบที่จะลงเอยในที่เก็บข้อมูลดังนั้นคณิตศาสตร์จึงผิดจริง ฉันคิดว่านี่เป็นคำตอบที่ดี
poulejapon

@pouejapon: ถูกต้อง
Neil G

คำตอบนี้ฟังดูดีมาก ปัญหาคือ - มันไม่เร็วจริงๆ ฉันใช้สิ่งนี้ใน C99 (ดูคำตอบของฉัน) และมันก็เต้นง่ายstd::sort()แต่ก็ช้ากว่าโซลูชัน Radixsort ของ Alexandru
Sven Marnach

2

แนวคิดการปรับให้เหมาะสมในระดับต่ำหนึ่งแนวคิดเพื่อให้พอดีกับสองเท่าในการลงทะเบียน SSE ดังนั้นแต่ละเธรดจะทำงานกับสองรายการในเวลาเดียวกัน นี่อาจจะซับซ้อนสำหรับอัลกอริทึมบางอย่าง

สิ่งที่ต้องทำก็คือเรียงลำดับอาร์เรย์ในแคชที่เป็นมิตรกับแคชแล้วรวมผลลัพธ์เข้าด้วยกัน ควรใช้สองระดับ: เช่น 4 KB แรกสำหรับ L1 และ 64 KB สำหรับ L2

สิ่งนี้ควรเป็นมิตรกับแคชมากเนื่องจากการจัดเรียงที่ฝากข้อมูลจะไม่ออกไปนอกแคชและการผสานสุดท้ายจะดำเนินการตามลำดับหน่วยความจำ

การคำนวณวันนี้มีราคาถูกกว่าการเข้าถึงหน่วยความจำมาก อย่างไรก็ตามเรามีรายการจำนวนมากดังนั้นจึงเป็นการยากที่จะบอกว่าขนาดอาเรย์คืออะไรเมื่อการเรียงลำดับที่ทราบแคชเป็นใบ้ช้ากว่าเวอร์ชันที่ไม่ซับซ้อนที่ไม่ทราบแคช

แต่ฉันจะไม่ดำเนินการตามที่กล่าวมาเนื่องจากฉันจะทำใน Windows (VC ++)


2

นี่คือการดำเนินการจัดเรียงกลุ่มการสแกนเชิงเส้น ฉันคิดว่ามันเร็วกว่าการใช้เธรดเดี่ยวปัจจุบันทั้งหมดยกเว้นการเรียงลำดับแบบ Radix ควรมีระยะเวลาคาดหวังแบบเส้นตรงถ้าฉันประมาณค่า cdf อย่างถูกต้องเพียงพอ (ฉันใช้การประมาณค่าเชิงเส้นที่พบบนเว็บ) และไม่ได้ทำผิดพลาดใด ๆ ที่อาจทำให้เกิดการสแกนมากเกินไป:

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

using std::fill;

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

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

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

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

1
ฉันจะลองในวันนี้เมื่อฉันกลับถึงบ้าน ในระหว่างนี้ฉันจะบอกได้ไหมว่ารหัสของคุณน่าเกลียดมาก? :-D
static_rtti

3.071s! ไม่เลวสำหรับโซลูชันแบบเธรดเดียว!
static_rtti

2

ฉันไม่รู้ว่าทำไมฉันไม่สามารถแก้ไขโพสต์ก่อนหน้าของฉันได้ดังนั้นนี่คือเวอร์ชั่นใหม่เร็วกว่า 0,2 วินาที (แต่เร็วขึ้นประมาณ 1.5 วินาทีในเวลา CPU (ผู้ใช้)) วิธีการแก้ปัญหานี้มี 2 โปรแกรมอันดับแรกทำการคำนวณควอนไทล์สำหรับการแจกแจงแบบปกติสำหรับการจัดเรียงถังและเก็บไว้ในตารางดัชนีถัง [t * double] = โดยที่สเกลเป็นจำนวนโดยพลการ จากนั้นโปรแกรมหลักสามารถใช้ข้อมูลนี้เพื่อวางคู่ในที่ฝากข้อมูลที่ถูกต้อง มีหนึ่งข้อเสียเปรียบถ้าข้อมูลไม่ Gaussian มันจะไม่ทำงานอย่างถูกต้อง (และยังมีโอกาสเกือบจะเป็นศูนย์ในการทำงานอย่างไม่ถูกต้องสำหรับการแจกแจงแบบปกติ) แต่การปรับเปลี่ยนสำหรับกรณีพิเศษนั้นง่ายและรวดเร็ว (จำนวนถังตรวจสอบเท่านั้น :: การจัดเรียง ())

การคอมไพล์: g ++ => โปรแกรมผู้ช่วยhttp://pastebin.com/WG7pZEzH

g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZPโปรแกรมการเรียงลำดับหลัก


1.621s! ฉันคิดว่าคุณเป็นผู้นำ แต่ฉันอย่างรวดเร็วสูญเสียการติดตามที่มีทุกคำตอบเหล่านี้ :)
static_rtti

2

นี่คือวิธีแก้ปัญหาแบบลำดับอื่น อันนี้ใช้ความจริงที่ว่าองค์ประกอบต่าง ๆ นั้นมีการแจกแจงแบบปกติและฉันคิดว่าโดยทั่วไปแล้วความคิดนั้นจะถูกนำมาใช้ในการเรียงลำดับใกล้เคียงกับเวลาเชิงเส้น

อัลกอริทึมเป็นดังนี้:

  • CDF โดยประมาณ (ดูphi()ฟังก์ชันในการนำไปใช้)
  • สำหรับองค์ประกอบทั้งหมดคำนวณตำแหน่งโดยประมาณในอาร์เรย์ที่เรียงลำดับ: size * phi(x)
  • วางองค์ประกอบในอาร์เรย์ใหม่ใกล้กับตำแหน่งสุดท้าย
    • ในอาร์เรย์ปลายทางการนำไปใช้งานของฉันมีช่องว่างอยู่ภายในดังนั้นฉันจึงไม่ต้องเปลี่ยนองค์ประกอบมากเกินไปเมื่อแทรก
  • ใช้ส่วนแทรกเพื่อเรียงลำดับองค์ประกอบสุดท้าย (ส่วนแทรกเป็นเส้นตรงถ้าระยะห่างจากตำแหน่งสุดท้ายมีขนาดเล็กกว่าค่าคงที่)

โชคไม่ดีค่าคงที่ซ่อนเร้นนั้นค่อนข้างใหญ่และการแก้ปัญหานี้ช้ากว่าอัลกอริธึมการเรียงตัวแบบ Radix


1
2.470s! ความคิดที่ดีมาก มันไม่สำคัญว่าการแก้ปัญหาไม่ได้เร็วที่สุดถ้าความคิดที่น่าสนใจ :)
static_rtti

1
นี่เป็นสิ่งเดียวกับของฉัน แต่การจัดกลุ่มการคำนวณ phi ด้วยกันและการเปลี่ยนแปลงร่วมกันเพื่อประสิทธิภาพแคชที่ดีขึ้นใช่ไหม
พูดถึงเรื่อง

@jonderry: ฉันอัปเกรดโซลูชันของคุณแล้วตอนนี้ฉันเข้าใจแล้วว่ามันทำอะไร ไม่ได้ตั้งใจจะขโมยความคิดของคุณ ฉันรวมการใช้งานของคุณไว้ในชุดการทดสอบ
Alexandru

2

รายการโปรดส่วนตัวของฉันโดยใช้เธรด Building Blocks ของ Intel ได้รับการโพสต์แล้ว แต่นี่เป็นโซลูชันขนานดิบโดยใช้ JDK 7 และ fork / join API ใหม่:

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


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

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

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

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

        ForkJoinPool mainPool = new ForkJoinPool();

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

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

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

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

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

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

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

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

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

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

        --left;
        ++right;

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

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

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

ข้อจำกัดความรับผิดชอบที่สำคัญ : ฉันใช้การปรับแบบเรียงลำดับอย่างรวดเร็วสำหรับการแยก / เข้าร่วมจาก: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel

ในการรันสิ่งนี้คุณต้องมีรุ่นเบต้าของ JDK 7 (http://jdk7.java.net/download.html)

ในควอดคอร์ i7 Core III ของฉัน (OS X) 2.93Ghz ของฉัน:

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

Update2:

ฉันเรียกใช้รหัสในหนึ่งใน 12 แกนเครื่องของเราด้วยการปรับเปลี่ยนเล็กน้อยเพื่อตั้งจำนวนแกนคงที่ สิ่งนี้ให้ผลลัพธ์ต่อไปนี้:

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

ในระบบนี้ฉันยังลองรุ่น Python ซึ่งใช้เวลา 1m2.994s และรุ่น C ++ ของ Zjarek ซึ่งใช้เวลา 1.925s (ด้วยเหตุผลบางอย่างเวอร์ชัน C ++ ของ Zjarek ดูเหมือนว่าจะทำงานได้เร็วกว่าบนคอมพิวเตอร์ 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

ในกรณีนี้เวอร์ชัน C ++ ของ Zjarek ใช้เวลา 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

ในกรณีนี้เวอร์ชัน C ++ ของ Zjarek คือ 6.044 วินาที ฉันไม่ได้ลอง Python

เวอร์ชัน C ++ นั้นสอดคล้องกับผลลัพธ์ของมันมากโดยที่ Java จะแกว่งไปมาเล็กน้อย เริ่มแรกจะมีประสิทธิภาพเพิ่มขึ้นเล็กน้อยเมื่อปัญหาใหญ่ขึ้น แต่ก็มีประสิทธิภาพลดลงอีกครั้ง


1
รหัสนี้ไม่ได้แยกค่าคู่อย่างถูกต้องสำหรับฉัน Java 7 จำเป็นต้องมีการวิเคราะห์ค่าจากไฟล์อย่างถูกต้องหรือไม่?
พูดถึงเรื่อง

1
อ่าฉันโง่ ฉันลืมตั้ง endianness อีกครั้งหลังจากที่ฉัน refactored รหัส IO จากหลายบรรทัดหนึ่ง Java 7 เป็นสิ่งจำเป็นโดยปกติเว้นแต่คุณจะเพิ่ม fork / join แยกต่างหากใน Java 6 แน่นอน
arjan

3.411s บนเครื่องของฉัน ไม่เลว แต่ช้ากว่าโซลูชัน java ของ koumes21 :)
static_rtti

1
ฉันจะลองใช้วิธีแก้ปัญหาของ koumes21 ที่นี่เช่นกันเพื่อดูว่ามีความแตกต่างสัมพัทธ์อยู่ในระบบของฉันอย่างไร อย่างไรก็ตามไม่มีความละอายในการ 'หลุด' จาก koumes21 เพราะมันเป็นทางออกที่ฉลาดกว่ามาก นี่เป็นเพียงมาตรฐานเกือบรวดเร็วเรียงโยนลงไปในส้อม / เข้าร่วมสระว่ายน้ำ;)
Arjan

1

เวอร์ชันที่ใช้ pthreads แบบดั้งเดิม รหัสสำหรับการรวมที่คัดลอกมาจากคำตอบของ Guvante g++ -O3 -pthreadคอมไพล์ด้วย

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

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

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


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

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


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

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

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

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

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

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

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

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

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

  pthread_attr_destroy(&attributes);
  return 0;
}  

บนแล็ปท็อปของฉันฉันได้รับผลลัพธ์ต่อไปนี้:

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

1

นี่คือการใช้ C99 ตามลำดับที่พยายามใช้ประโยชน์จากการกระจายที่รู้จัก โดยทั่วไปแล้วมันจะทำการจัดเรียงข้อมูลรอบเดียวโดยใช้ข้อมูลการแจกจ่ายจากนั้นสองสามรอบของการจัดเรียงข้อมูลด่วนแต่ละครั้งโดยสมมติว่าการแจกจ่ายแบบสม่ำเสมอภายในขอบเขตของที่เก็บข้อมูลและสุดท้ายก็เป็นการเลือกแก้ไขที่คัดลอกเพื่อคัดลอกข้อมูล quicksort จดจำจุดแยกดังนั้นการเรียงลำดับการเลือกจำเป็นต้องใช้กับ cunks ขนาดเล็กเท่านั้น และทั้ง ๆ ที่ความซับซ้อนนั้นมันไม่ได้เร็วนัก

ในการประเมินΦอย่างรวดเร็วค่าจะถูกสุ่มตัวอย่างในไม่กี่จุดและหลังจากนั้นจะใช้การประมาณเชิงเส้นเท่านั้น ที่จริงแล้วมันไม่สำคัญว่า evalu จะได้รับการประเมินอย่างถูกต้องหรือไม่ตราบใดที่การประมาณค่าเป็นแบบโมโนโทนิคอย่างเคร่งครัด

ขนาดของถังขยะถูกเลือกเพื่อไม่ให้มีโอกาสล้นถังขยะ แม่นยำยิ่งขึ้นด้วยพารามิเตอร์ปัจจุบันโอกาสที่ชุดข้อมูลขององค์ประกอบ 50000000 จะทำให้เกิดการล้นถังขยะคือ 3.65e-09 (สามารถคำนวณได้โดยใช้ฟังก์ชันการอยู่รอดของการแจกแจงปัวซง )

เพื่อรวบรวมโปรดใช้

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

เนื่องจากมีการคำนวณมากขึ้นกว่าในโซลูชันอื่น ๆ ธงคอมไพเลอร์เหล่านี้จึงจำเป็นต้องทำให้อย่างน้อยสมเหตุสมผลอย่างรวดเร็ว หากไม่มี-msse3การแปลงdoubleให้intช้าลงจริงๆ หากสถาปัตยกรรมของคุณไม่รองรับ SSE3 การแปลงเหล่านี้สามารถทำได้โดยใช้lrint()ฟังก์ชัน

รหัสค่อนข้างน่าเกลียด - ไม่แน่ใจว่าสิ่งนี้ตรงตามข้อกำหนดของการ "อ่านได้อย่างสมเหตุสมผล" ...

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

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

double phi_vals[PHI_VALS];

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

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

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

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

4.098s! ฉันต้องเพิ่ม -lm เพื่อคอมไพล์มัน (สำหรับเอลฟ์)
static_rtti

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

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

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

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

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


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


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


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

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

        return 0;
}

สิ่งนี้ใช้ erf () เพื่อวางแต่ละองค์ประกอบอย่างเหมาะสมลงในถังขยะแล้วเรียงลำดับแต่ละถัง มันทำให้อาร์เรย์ทั้งหมดในสถานที่

ผ่านครั้งแรก: docensus () นับจำนวนองค์ประกอบในแต่ละถัง

รอบที่สอง: partition () อนุญาตให้อาร์เรย์วางแต่ละองค์ประกอบลงในถังขยะที่เหมาะสม

รอบที่สาม: sortbins () ทำการ qsort บนแต่ละ bin

มันช่างไร้เดียงสาและเรียกฟังก์ชั่นราคาแพง erf () สองครั้งสำหรับทุกค่า การผ่านครั้งแรกและครั้งที่สามนั้นสามารถขนานกันได้ อย่างที่สองคืออนุกรมสูงและอาจช้าลงโดยรูปแบบการเข้าถึงหน่วยความจำแบบสุ่มสูง นอกจากนี้ยังอาจคุ้มค่าที่จะแคชหมายเลข bin ของแต่ละคู่ขึ้นอยู่กับพลังงานของ CPU ต่ออัตราส่วนความเร็วหน่วยความจำ

โปรแกรมนี้ให้คุณเลือกจำนวนถังขยะที่จะใช้ เพียงเพิ่มหมายเลขที่สองในบรรทัดคำสั่ง ฉันรวบรวมมันด้วย gcc -O3 แต่เครื่องของฉันอ่อนแอฉันจึงไม่สามารถบอกคุณได้ว่ามีประสิทธิภาพดีแค่ไหน

แก้ไข:กะเทย! โปรแกรม C ของฉันเปลี่ยนเป็นโปรแกรม C ++ ได้อย่างน่าอัศจรรย์โดยใช้ std :: sort!


คุณสามารถใช้phiเพื่อ stdnormal_cdf ที่เร็วขึ้น
Alexandru

ฉันควรใส่ถังขยะกี่อัน?
static_rtti

@Alexandru: ฉันเพิ่มการประมาณเชิงเส้นทีละชิ้นใน normcdf และได้รับความเร็วประมาณ 5% เท่านั้น
frud

@static_rtti: คุณไม่ต้องใส่อะไรเลย โดยค่าเริ่มต้นรหัสจะเลือกจำนวนของถังขยะดังนั้นขนาดช่องเก็บเฉลี่ยคือ 10/11 ของ 128kb ถังขยะน้อยเกินไปและคุณไม่ได้รับประโยชน์จากการแบ่งพาร์ติชัน มีมากเกินไปและเฟสพาร์ติชันจะชะงักเนื่องจากการล้นแคช
frud

10.6s! ฉันพยายามเล่นด้วยจำนวนของถังขยะและฉันได้ผลลัพธ์ที่ดีที่สุดด้วย 5,000 (สูงกว่าค่าเริ่มต้นเล็กน้อยที่ 3356) ฉันต้องบอกว่าฉันคาดว่าจะเห็นประสิทธิภาพที่ดีขึ้นสำหรับโซลูชันของคุณ ... บางทีมันอาจเป็นความจริงที่ว่าคุณกำลังใช้ qsort แทน std :: sort ที่เป็นไปได้ของโซลูชั่น C ++?
static_rtti

1

ดูที่การจัดเรียง Radix โดย Michael Herf ( Radix Tricks ) ในการเรียงลำดับเครื่องของฉันเร็วขึ้น 5 เท่าเมื่อเทียบกับstd::sortอัลกอริทึมในคำตอบแรกของฉัน RadixSort11ชื่อของฟังก์ชั่นการเรียงลำดับคือ

int main(void)
{
    std::ifstream ifs("C:\\Temp\\gaussian.dat", std::ios::binary | std::ios::in);
    std::vector<float> v;
    v.reserve(50000000);
    double d;
    while (ifs.read(reinterpret_cast<char*>(&d), sizeof(double)))
        v.push_back(static_cast<float>(d));
    std::vector<float> vres(v.size(), 0.0);
    clock_t c0 = clock();
    RadixSort11(&v[0], &vres[0], v.size());
    std::cout << "Finished after: "
              << static_cast<double>(clock() - c0) / CLOCKS_PER_SEC << std::endl;
    return 0;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.