Конкурс: найшвидший спосіб сортування великого масиву даних, розподілених Гауссом


71

Після інтересу до цього питання , я подумав, що було б цікаво зробити відповіді трохи більш об'єктивними та кількісними, запропонувавши конкурс.

Ідея проста: я створив двійковий файл, що містить 50 мільйонів подвійних розподілених гауссів (у середньому: 0, stdev 1). Мета полягає в тому, щоб зробити програму, яка буде сортувати їх у пам'яті якомога швидше. Дуже проста реалізація посилань у python займає 1m4s. Як низько ми можемо піти?

Правила такі: відповідь з програмою, яка відкриває файл "gaussian.dat" і сортування номерів у пам'яті (не потрібно їх виводити), та інструкції щодо створення та запуску програми. Програма повинна мати можливість працювати на моїй машині Arch Linux (тобто ви можете використовувати будь-яку мову програмування або бібліотеку, яку легко встановити в цій системі).

Програма повинна бути легко читаною, щоб я міг переконатися, що це безпечно запускати (будь-яке рішення для асемблера, будь ласка!).

Я запускатиму відповіді на своїй машині (чотирьохядерний ядер, 4 гігабайти оперативної пам’яті). Швидке рішення отримає прийняту відповідь і 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)

EDIT: всього 4 ГБ оперативної пам’яті, вибачте

РЕДАКТИКА №2: Зауважте, що суть конкурсу полягає в тому, щоб ми могли використовувати попередню інформацію про дані . це не повинно бути підступним збігом між різними реалізаціями мови програмування!


1
Візьміть кожне значення і перемістіть його безпосередньо у його "очікувану" позицію, повторіть для зміщеного значення. Не знаєте, як з цим вирішити пару проблем. Після закінчення сортування бульбашок до повного завершення (потрібно зробити пару пропусків).

1
Я відправлю завтра рішення про сортування відра завтра ввечері, якщо його до цього часу не закрили :)

1
@static_rtti - як важкий користувач CG, це саме те, що ми «любимо» ламати на CG.SE. Перемістіть це на CG до будь-яких модів читання, не закривайте його.
arrdem

1
Ласкаво просимо до CodeGolf.SE! Я очистив багато коментарів з оригіналу SO щодо того, де це робиться чи не належить, і повторно помітив, щоб бути ближче до основного потоку CodeGolf.SE.
dmckee

2
Тут є одна хитра проблема - ми шукаємо об'єктивні критерії виграшу, і "найшвидший" вводить залежність від платформи ... чи алгоритм O (n ^ {1.2}), реалізований на віртуальній машині cpython, бив O (n ^ {1.3} ) алгоритм з подібною постійною, реалізованою в c? Я, як правило, пропоную кілька дискусій щодо характеристик ефективності кожного рішення, оскільки це може допомогти людям судити про те, що відбувається.
dmckee

Відповіді:


13

Ось рішення в C ++, яке спочатку розділяє числа на відра з однаковою очікуваною кількістю елементів, а потім сортує кожне відро окремо. Він попередньо обчислює таблицю функції кумулятивного розподілу на основі деяких формул з Вікіпедії, а потім інтерполює значення з цієї таблиці, щоб отримати швидке наближення.

Кілька кроків виконуються в декількох потоках, щоб використовувати чотири ядра.

#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

EDIT: Усі відра тепер розміщуються в одному масиві, щоб усунути необхідність копіювання відра назад в масив. Також було зменшено розмір таблиці з попередньо обчисленими значеннями, оскільки значення є досить точними. І все-таки, якщо я зміню кількість відра, що перевищує 256, програма запускає більше часу, ніж із цією кількістю відра.

EDIT: той самий алгоритм, інша мова програмування. Я використовував C ++ замість Java, а час роботи скоротився з ~ 3,2s до ~ 2,35s на моїй машині. Оптимальна кількість відра все ще становить близько 256 (знову ж таки, на моєму комп’ютері).

До речі, tbb дійсно є приголомшливим.

EDIT: Я надихнувся чудовим рішенням Олександра і замінив std :: sort на останній фазі модифікованою версією свого типу radix. Я використовував інший метод, щоб мати справу з позитивними / від’ємними числами, навіть якщо йому потрібно більше проходів через масив. Я також вирішив точно сортувати масив і видалити сортування вставки. Пізніше я проведу деякий час, перевіряючи, як ці зміни впливають на продуктивність і, можливо, повертають їх. Однак, використовуючи радіо-сортування, час зменшився з ~ 2,35s до ~ 1,63s.


Приємно. Я отримав 3.055 на моєму. Найнижчим я зміг дістати свій 6,3. Я перебираю ваші, щоб покращити статистику. Чому ви вибрали 256 як кількість відра? Я спробував 128 і 512, але 256 працювали найкраще.
Скотт

Чому я вибрав 256 як кількість відра? Я спробував 128 і 512, але 256 працювали найкраще. :) Я знайшов це емпірично, і я не впевнений, чому збільшення кількості відра уповільнює алгоритм - розподіл пам’яті не повинно займати так довго. Може щось пов’язане з розміром кешу?
k21

2.725 на моїй машині. Дуже приємно для рішення java з урахуванням часу завантаження JVM.
static_rtti

2
Я переключив ваш код на використання пакетів nio за рішенням мого та Аряна (використав його синтаксис, оскільки він чистіший за мій) і зміг отримати його на 3 секунди швидше. У мене є ssd, мені цікаво, які наслідки можуть бути, якщо ні. Це також позбавляється від деяких ваших бітових подруг. Модифіковані розділи тут.
Скотт

3
Це найшвидше паралельне рішення на моїх тестах (16core cpu). 1,22с далеко від 1,94с другого місця.
Олександру

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.086, що робить вас нинішнім керівником! Але я впевнений, що ми можемо зробити краще :)

1
Невже ви можете спробувати видалити другий потрійний оператор і просто повернути 1 для цього випадку, оскільки випадкові подвоєння, як і не рівні між собою в цьому обсязі даних.
Кодизм

@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 секунди. Якщо розділити за допомогою дистрибутиву, ви не можете просто використовувати масив, тому є додаткові накладні витрати. Мені не зрозуміло, коли значення буде розміщене в коробці / без коробки в масштабі.

Я використовую scala 2.9 для паралельного збору. Ви можете просто завантажити дистрибутив tar.gz.

Для компіляції: scalac SortFile.scala (я просто скопіював його безпосередньо в папку scala / bin.

Для запуску: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (я запустив його з 2 гігами оперативної пам’яті і отримав приблизно в той же час)

Редагувати: Видалено allocateDirect, повільніше, ніж просто розподілити. Видалено примітку початкового розміру для буферів масиву. Фактично змусив її прочитати цілі 50000000 значень. Перепишіть, щоб сподіватися уникнути проблем із автобоксінгом (все-таки повільніше, ніж наївні c)

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


object SortFile {

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

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

def partition(dbl:Double): Int = { 

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

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

}

  def main(args: Array[String])
    { 

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

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

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

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

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

  }
}

1
8.185с! Приємно для рішення масштабу, я думаю ... Крім того, браво за надання першого рішення, яке фактично певним чином використовує розподіл Гаусса!

1
Я мав на меті змагатися лише з рішенням c #. Не знаю, я бив c / c ++. Також .. це поводиться набагато інакше, ніж для мене. Я використовую openJDK на своєму кінці, і це набагато повільніше. Цікаво, чи додасть більше розділів допоможе у вашому оточенні.
Скотт

9

Просто помістіть це у файл cs і теоретично компілюйте його з csc: (Потрібно моно)

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

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

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

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

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

Чи можу я запустити ваші рішення з Mono? Як мені це зробити?

Не використовував Mono, не думав про це, ти повинен мати можливість скласти F # і потім запустити його.

1
Оновлено для використання чотирьох потоків для підвищення продуктивності. Тепер дає мені 6 сек. Зауважте, що це можна значно покращити (вірогідніше 5 секунд), якщо ви використовуєте лише один запасний масив і уникаєте ініціалізації тону пам'яті до нуля, що робиться CLR, оскільки все записується як мінімум один раз.

1
9.598s на моїй машині! Ви нинішній керівник :)

1
Мама сказала мені триматися подалі від хлопців з Моно!

8

Оскільки ви знаєте, що таке розподіл, ви можете використовувати сортування O (N) прямого індексування. (Якщо вам цікаво, що це таке? Припустімо, у вас колода з 52 карт, і ви хочете сортувати її. Просто майте 52 контейнери і кидайте кожну карту у свій власний контейнер.)

У вас є 5е7 пар. Виділіть масив результатів R 5e7 пар. Візьміть кожне число xі дістаньте i = phi(x) * 5e7. В основному так і роблять R[i] = x. Майте спосіб вирішувати зіткнення, наприклад переміщення числа, з яким воно може зіткнутися (як у простому хеш-кодуванні). Крім того, ви можете зробити R в кілька разів більшим, заповненим унікальним порожнім значенням. Наприкінці ви просто змітаєте елементи Р.

phi- це лише гауссова функція кумулятивного розподілу. Він перетворює гауссова розподілене число між +/- нескінченності в рівномірне розподілене число між 0 і 1. Простий спосіб обчислити це за допомогою пошуку таблиць та інтерполяції.


3
Будьте уважні: ви знаєте приблизний розподіл, а не точний розподіл. Ви знаєте, що дані були згенеровані за допомогою закону Гаусса, але оскільки вони кінцеві, вони точно не відповідають Гауссу.

@static_rtti: У цьому випадку необхідне наближення phi створить більше клопоту, ніж будь-які порушення в ІМО набору даних.

1
@static_rtti: це не повинно бути точним. Він повинен лише розповсюджувати дані, щоб вони були приблизно однаковими, тому в деяких місцях вони не збираються занадто багато.

Припустимо, у вас є 5e7 парних. Чому б просто не зробити кожен запис у R вектором, скажімо, 5е6 векторів подвійних. Потім натисніть назад кожен подвійний у відповідному векторі. Сортуйте вектори, і ви закінчите. Це має займати лінійний час у розмірі вхідних даних.
Ніл Г

Насправді я бачу, що mdkess вже придумала це рішення.
Ніл Г

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

Зауважимо, що це рішення має лінійну часову складність (оскільки використовує спеціальне подання пар).

EDIT : Виправлено порядок збільшення елементів.

EDIT : Поліпшена швидкість майже на півсекунди.

EDIT : Покращена швидкість ще на 0,7 секунди. Зробив алгоритм більш зручним для кешу.

EDIT : Покращена швидкість ще на 1 секунду. Оскільки всього 50 000 000 елементів, я можу частково сортувати мантію і використовувати сортування вставок (який є кешованим), щоб виправити непомітні елементи. Ця ідея знімає дві ітерації з останнього радіоскопічного сортування.

EDIT : на 0,16 менше секунд. Перший std :: reverse може бути усунутий, якщо порядок сортування відмінено.


Тепер це стає цікавим! Що це за алгоритм сортування?
static_rtti

2
Найменш значущий сорт радікса . Можна сортувати мантісу, потім показник, потім знак. Представлений тут алгоритм робить цю ідею ще на крок. Її можна паралельно використовувати, використовуючи ідею розділення, надану в іншій відповіді.
Олександру

Досить швидко для одного різьбового рішення: 2,555! Як ви думаєте, ви могли б змінити своє рішення, щоб скористатися тим, що дані зазвичай поширюються? Можливо, ви могли б зробити краще, ніж нині найкращі багатопотокові рішення.
static_rtti

1
@static_rtti: Я бачу, що Damascus Steel вже опублікував багатопоточну версію цієї реалізації. Я вдосконалив поведінку кешування цього алгоритму, тому вам слід отримати кращі терміни тепер. Перевірте цю нову версію.
Олександру

2
1.459 в моїх останніх тестах. Хоча це рішення не є переможцем за моїми правилами, воно дійсно заслуговує на великі кудо. Вітаємо!
static_rtti

6

Прийняття рішення Крістіана Аммера та паралельне його співпраці з інтелектуальними блоками 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;
}

Якщо у вас є доступ до бібліотеки продуктивних примітивів (IPP) Intel, ви можете використовувати її радіаційний сорт. Просто замініть

#include <tbb/parallel_sort.h>

з

#include "ipps.h"

і

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

з

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

На моєму двоядерному ноутбуці терміни є

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

1
2.958с! TBB здається досить крутим і простим у використанні!

2
ТББ абсурдно приголомшливий. Це саме правильний рівень абстракції для алгоритмічної роботи.
drxzcl

5

Як щодо реалізації паралельної швидкості, яка вибирає свої основні значення на основі статистики розподілу, забезпечуючи тим самим розділи за рівними розмірами? Перший стрижень був би в середньому (нуль в цьому випадку), наступна пара буде на 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)! І приємне використання гауссового розподілу :)
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;
}

Я не можу з упевненістю сказати, скільки часу займає, тому що на моїй машині є лише 1 Гб, і за допомогою даного коду Python я міг створити gaussian.datфайл лише з 25-мільйонними подвійними (без отримання помилки пам'яті). Але мене дуже цікавить, як довго працює алгоритм std :: sort.


6.425s! Як і очікувалося, C ++ світить :)

@static_rtti: Я спробував алгоритм Tensort swensons (як це запропонував Матьє М. у першому запитанні ). Мені довелося внести деякі зміни у sort.hфайл, щоб компілювати його з C ++. Це було приблизно в два рази повільніше , ніж std::sort. Не знаєте чому, можливо, через оптимізацію компілятора?
Крістіан Аммер

4

Ось суміш сорту радіуса Александру з розумним поворотом Зярека. Скомпілюйте його

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

Ви можете змінити розмір радіусу, визначивши КРОК (наприклад, додати -DSTEP = 11). Я знайшов найкращий для свого ноутбука 8 (за замовчуванням).

За замовчуванням вона розбиває проблему на 4 частини і виконує її на кілька потоків. Ви можете змінити це, передавши параметр глибини в командний рядок. Тож якщо у вас є два ядра, запустіть його як

sorter_gaussian_radix 50000000 1

і якщо у вас 16 ядер

sorter_gaussian_radix 50000000 4

Максимальна глибина зараз становить 6 (64 нитки). Якщо ви покладете занадто багато рівнів, ви просто сповільнить код.

Одне, що я також спробував, - це сорт радіації з бібліотеки Intel Performance Primitive (IPP). Впровадження Олександру чудово стримує IPP, IPP на 30% повільніше. Ця зміна також включена сюди (коментується).

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

#ifndef STEP
#define STEP 8
#endif

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

clock_t c1, c2, c3, c4, c5;

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


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

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


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

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

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

      c1=clock();

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

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

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

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

      c2=clock();

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

      c3=clock();

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

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

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

      c4=clock();

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

    }
}


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

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

  clock_t c0 = clock();

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

  // ippsSortRadixAscend_64f_I(dbuf,copy,size);

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

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

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

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

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

EDIT : Я впровадив вдосконалення кеш-пам'яті Олександру, і це знищило приблизно 30% часу на моїй машині.

EDIT : Це реалізує рекурсивний тип, тому він повинен добре працювати на 16-ти основних машинах Александру. Він також використовує останнє вдосконалення Олександру та видаляє один із реверсів. Для мене це дало 20% покращення.

EDIT : виправлена ​​помилка знаків, яка викликала неефективність, коли є більше 2 ядер.

EDIT : лямбда видалено, тож вона буде компілюватися зі старими версіями gcc. Він включає коментовану варіацію коду IPP. Я також зафіксував документацію для роботи на 16 ядрах. Наскільки я можу сказати, це найшвидша реалізація.

EDIT : Виправлена ​​помилка, коли STEP не є 8. Збільшено максимальну кількість потоків до 64. Додано деяку інформацію про час.


Приємно. Сортування Radix дуже недоброзичливо кеш. Подивіться, чи зможете ви отримати кращі результати, змінивши step(11 було оптимальним на моєму ноутбуці).
Олександру

У вас помилка: int cnt[mask]повинна бути int cnt[mask + 1]. Для кращих результатів використовуйте фіксовану величину int cnt[1 << 16].
Олександру

Я спробую всі ці рішення пізніше сьогодні, коли повернусь додому.
static_rtti

1.534s !!! Я думаю, у нас є лідер :-D
static_rtti

@static_rtti: Ви можете спробувати це ще раз? Він став значно швидшим, ніж останній раз, коли ви його спробували. На моїй машині це значно швидше, ніж будь-яке інше рішення.
Дамаск Сталь

2

Я думаю, це дійсно залежить від того, що ти хочеш зробити. Якщо ви хочете сортувати купу гаусів, то це вам не допоможе. Але якщо ви хочете купу відсортованих гаївців, це буде. Навіть якщо це трохи не вистачає проблеми, я думаю, що буде цікаво порівняти з фактичними процедурами сортування.

Якщо ви хочете, щоб щось було швидким, робіть менше.

Замість того, щоб генерувати купу випадкових вибірок із звичайного розподілу, а потім сортувати, ви можете генерувати купу зразків із звичайного розподілу у відсортованому порядку.

Ви можете використовувати тут рішення для генерації n однакових випадкових чисел у відсортованому порядку. Тоді ви можете використовувати зворотний cdf (scipy.stats.norm.ppf) нормального розподілу, щоб перетворити рівномірні випадкові числа у числа з нормального розподілу через вибіркове обернене перетворення .

import scipy.stats
import random

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

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

Якщо ви хочете, щоб ваші руки були більш брудними, я б припустив, що ви, можливо, зможете прискорити безліч обернених обчислень у форматі PDF, використовуючи якийсь ітераційний метод та використовуючи попередній результат у якості початкової здогадки. Оскільки здогадки будуть дуже близькими, ймовірно, одна ітерація дасть вам велику точність.


2
Приємна відповідь, але це було б обманом :) Ідея мого питання полягає в тому, що хоча алгоритмам сортування приділяється величезна увага, майже немає літератури про використання попередніх знань про дані для сортування, навіть незважаючи на декілька робіт Вирішене питання повідомило про хороші успіхи. Тож давайте подивимось, що можливо!

2

Спробуйте це змінення рішення Гуванте з цим 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.933с. Трохи швидше :)

2

Оскільки ви знаєте розподіл, моя ідея полягала б у тому, щоб зробити k відра, кожне з однаковою очікуваною кількістю елементів (оскільки ви знаєте розподіл, ви можете це обчислити). Потім в O (n) час підмітайте масив і покладіть елементи у свої відра.

Потім одночасно сортуйте відра. Припустимо, у вас є k відра і n елементів. Для сортування відра знадобиться (n / k) lg (n / k) час. Тепер припустимо, що у вас є p процесори, які ви можете використовувати. Оскільки відра можна сортувати самостійно, у вас є мультиплікатор ceil (k / p), з яким можна мати справу. Це дає остаточний час виконання n + ceil (k / p) * (n / k) lg (n / k), що має бути набагато швидшим, ніж n lg n, якщо ви виберете k добре.


Я думаю, що це найкраще рішення.
Ніл Г

Ви точно не знаєте кількість елементів, які опиняться у відрі, тому математика насправді неправильна. Попри це, я думаю, що це хороша відповідь.
poulejapon

@pouejapon: Ти маєш рацію.
Ніл Г

Ця відповідь звучить дуже приємно. Проблема в тому, що це не дуже швидко. Я реалізував це в C99 (див. Мою відповідь), і він, безумовно, легко перемагає std::sort(), але це набагато повільніше, ніж рішення Олександрського радиксорту.
Свен Марнах

2

Одна ідея оптимізації низького рівня полягає в тому, щоб помістити два парні в регістр SSE, так що кожен потік працював би з двома елементами одночасно. Це може бути складно зробити для деяких алгоритмів.

Інша річ - це сортування масиву в кеш-пам'яті, а потім об'єднання результатів. Слід використовувати два рівні: наприклад, спочатку 4 Кб для L1, а потім 64 Кб для 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.071с! Непогано для однониткового рішення!
static_rtti

2

Я не знаю, чому я не можу редагувати свою попередню публікацію, тому ось нова версія, на 0,2 секунди швидша (але приблизно на 1,5 с швидше за час процесора (користувач)). У цьому рішенні є 2 програми, спочатку попередньо підраховує квантові знаки для нормального розподілу для сортування відра, і зберігає їх у таблиці, t [подвійний * шкала] = індекс відра, де шкала є деяким довільним числом, що робить кастинг вдвічі можливим. Тоді основна програма може використовувати ці дані для встановлення пар у правильне відро. У нього є один недолік, якщо дані не є гауссовими, вони працюватимуть неправильно (а також є майже нульовий шанс працювати неправильно для нормального розподілу), але модифікація для спеціального випадку проста та швидка (лише кількість відра перевіряє та падає на std :: sort ()).

Компіляція: g ++ => http://pastebin.com/WG7pZEzH helper program

g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZP основна програма сортування


1.621s! Я думаю, що ти лідер, але я швидко втрачаю сліди з усіма цими відповідями :)
static_rtti

2

Ось ще одне послідовне рішення. У цьому використовується той факт, що елементи розподіляються нормально, і я думаю, що ідея загалом застосовна для сортування, близького до лінійного часу.

Алгоритм такий:

  • Орієнтовний CDF (див. phi()Функцію в реалізації)
  • Для всіх елементів обчисліть приблизну позицію в відсортованому масиві: size * phi(x)
  • Помістіть елементи в новий масив близько до їх кінцевого положення
    • У моєму призначенні масив реалізації має деякі прогалини, тому мені не потрібно зміщувати занадто багато елементів при вставці.
  • Використовуйте inserttsort для сортування кінцевих елементів (вставкаort лінійна, якщо відстань до остаточного положення менше постійної).

На жаль, прихована константа є досить великою, і це рішення вдвічі повільніше алгоритму сортування радіації.


1
2.470! Дуже приємні ідеї. Не має значення, що рішення не найшвидше, якщо ідеї цікаві :)
static_rtti

1
Це те саме, що і моє, але групувати обчислення phi разом і зрушення разом для кращої продуктивності кешу, правда?
веселощі

@jonderry: Я підтримав ваше рішення, тепер, коли я розумію, що це робить. Не мав на увазі вкрасти свою ідею. Я включив вашу реалізацію в свій (неофіційний) набір тестів
Олександру

2

Мій особистий фаворит, що використовує нитки Intel Building’s Threaded Building Blocks, вже опублікований, але ось суворе паралельне рішення з використанням JDK 7 та його нового API fork / join:

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


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

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

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

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

        ForkJoinPool mainPool = new ForkJoinPool();

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

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

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

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

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

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

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

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

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

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

        --left;
        ++right;

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

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

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

Важлива відмова від відповідальності : я скористався швидкою адаптацією сортування fork / join від: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel

Для цього вам потрібна бета-версія збірки JDK 7 (http://jdk7.java.net/download.html).

У моєму 2,93 ГГц чотирьохядерний i7 (OS X):

Посилання Python

time python sort.py 50000000
sorting...

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

Java JDK 7 вилка / приєднуйтесь

time java ForkJoinQuicksortTask 50000000
Starting parallel computation

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

Я також спробував зробити кілька експериментів з паралельним читанням і перетворенням байтів у парні, але я там не бачив різниці.

Оновлення:

Якщо хтось хоче експериментувати з паралельним завантаженням даних, наведена нижче версія паралельного завантаження. Теоретично це може змусити його трохи швидше, якщо ваш пристрій вводу-виводу має достатню паралельну ємність (зазвичай це SSD). Існує також деяка накладні витрати в створенні парного з байтів, так що потенційно може йти швидше паралельно. У моїх системах (Ubuntu 10.10 / Nehalem Quad / Intel X25M SSD та OS X 10.6 / i7 Quad / Samsung SSD) я не бачив реальної різниці.

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

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


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

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

       ForkJoinPool mainPool = new ForkJoinPool();

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

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

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

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

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

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

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

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

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

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

       --left;
       ++right;

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

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

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

}

class ReadAction extends RecursiveAction {

   private static final long serialVersionUID = -3498527500076085483L;

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

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

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

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

Оновлення2:

Я виконав код на одній з наших 12 основних машин для розробників з невеликою модифікацією, щоб встановити фіксовану кількість ядер. Це дало такі результати:

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

У цій системі я також спробував версію Python, яка займала 1м2.994, і версію C ++ Зярека, що займала 1.925 секунди (чомусь версія Zjarek C ++, здається, працює на комп'ютері static_rtti порівняно швидше).

Я також спробував, що сталося, якщо я подвоїв розмір файлу до 100 000 000 пар:

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

У цьому випадку версія C ++ Zjarek зайняла 3,968. Пітон тут зайняв занадто довго.

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
Ах, нерозумно мені. Я забув знову встановити витривалість після того, як я локально відновив код IO з декількох рядків в один. Як правило, Java 7 потрібна, якщо ви, звичайно, не додали форк / приєднатися окремо до Java 6.
arjan

3.411 на моїй машині. Непогано, але повільніше, ніж Java-рішення
koumes21

1
Я спробую рішення koumes21 тут теж локально, щоб побачити, які відносні відмінності в моїй системі. У будь-якому разі, немає сорому в тому, щоб «втратити» від koumes21, оскільки це набагато розумніше рішення. Це лише майже стандартний швидкий сорт, кинутий у вилку / приєднатися до пулу;)
arjan

1

Версія з використанням традиційних pthreads. Код для злиття скопійований з відповіді Гуванте. Компілювати з 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, яка намагається реально використати відомий дистрибутив. Це, в основному, один цикл сортування відра, використовуючи інформацію про розподіл, потім кілька раундів швидкості у кожному відрі, припускаючи рівномірний розподіл у межах ковша, і, нарешті, модифікований сортувальний сортування для копіювання даних назад у вихідний буфер. Квіксорт запам'ятовує розділені точки, тому сортування селекції потрібно працювати лише на невеликих кусках. І незважаючи на (через що) всю цю складність, це навіть не дуже швидко.

Для швидкого оцінювання the значення відбирають у кілька точок, а пізніше використовується лише лінійна інтерполяція. Насправді не має значення, чи точно оцінюється exactly, доки апроксимація суворо монотонна.

Розміри відрізників вибираються такими, що шанс переповнення контейнера є незначним. Точніше, при поточних параметрах ймовірність того, що набір даних з 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.098с! Мені довелося додати -lm, щоб компілювати його (для erf).
static_rtti

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

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

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

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

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


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


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


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

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

        return 0;
}

При цьому використовується erf () для належного розміщення кожного елемента у відро, а потім сортування кожного контейнера. Він зберігає масив повністю на своєму місці.

Перший прохід: docensus () підраховує кількість елементів у кожному контейнері.

Другий прохід: розділ () перетворює масив, розміщуючи кожен елемент у своєму правильному біні

Третій прохід: sortbins () виконує qsort на кожному біні.

Це наївно, і викликає дорогу функцію erf () двічі за кожне значення. Перший і третій проходи потенційно є паралельними. Друга є дуже послідовною і, ймовірно, уповільненою завдяки своїм дуже випадковим моделям доступу до пам'яті. Також може бути доцільним кеш-пам'ять кожного двомісного бін-номера, залежно від співвідношення швидкості процесора та швидкості пам’яті.

Ця програма дозволяє вибрати кількість контейнерів для використання. Просто додайте другий номер до командного рядка. Я компілював його з gcc -O3, але мій апарат настільки слабкий, що я не можу вам сказати про хороші показники продуктивності.

Редагувати: Пуф! Моя програма C магічно перетворилася на програму C ++ за допомогою std :: sort!


Ви можете використовувати phi для швидшого stdnormal_cdf.
Олександру

Скільки ящиків я повинен поставити, приблизно?
static_rtti

@ Александру: Я додав кусково-лінійне наближення до normcdf і отримав лише близько 5% швидкості.
frud

@static_rtti: Не потрібно ставити жодного. За замовчуванням код вибирає кількість бункерів, тому середній розмір контейнера становить 10/11 від 128 кбіт. Занадто мало контейнерів, і ви не отримаєте користі від перегородки. Занадто багато і фаза розділу розділяється через переповнення кешу.
frud

10,6с! Я спробував трохи пограти з кількістю бункерів, і я отримав найкращі результати з 5000 (трохи перевищує значення за замовчуванням 3356). Треба сказати, що від мене очікувалося набагато кращі показники для вашого рішення ... Можливо, це факт, що ви використовуєте qsort замість потенційно швидшого std :: свого роду C ++ рішень?
static_rtti

1

Погляньте на реалізацію сортування радикса Майкла Герфа ( 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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.