Учитывая интерес к этому вопросу , я подумал, что было бы интересно сделать ответы более объективными и количественными, предложив конкурс.
Идея проста: я сгенерировал двоичный файл, содержащий 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)
РЕДАКТИРОВАТЬ: только 4 ГБ оперативной памяти, извините
РЕДАКТИРОВАТЬ # 2: Обратите внимание, что смысл конкурса заключается в том, чтобы увидеть, можем ли мы использовать предварительную информацию о данных . это не должно быть отличным совпадением между различными реализациями языка программирования!
источник
Ответы:
Вот решение в C ++, которое сначала разбивает числа на сегменты с одинаковым ожидаемым количеством элементов, а затем сортирует каждый сегмент отдельно. Он предварительно вычисляет таблицу накопительной функции распределения на основе некоторых формул из Википедии, а затем интерполирует значения из этой таблицы, чтобы получить быстрое приближение.
Несколько шагов выполняются в нескольких потоках, чтобы использовать четыре ядра.
Чтобы скомпилировать и запустить его, используйте эту команду:
РЕДАКТИРОВАТЬ: Все сегменты теперь помещены в один и тот же массив, чтобы избавить от необходимости копировать сегменты обратно в массив. Также был уменьшен размер таблицы с предварительно вычисленными значениями, поскольку значения достаточно точны. Тем не менее, если я изменю количество сегментов выше 256, программа будет работать дольше, чем с таким количеством сегментов.
РЕДАКТИРОВАТЬ: один и тот же алгоритм, другой язык программирования. Я использовал C ++ вместо Java, и время работы на моей машине сократилось с ~ 3.2 с до 2.35 с. Оптимальное количество сегментов по-прежнему около 256 (опять же на моем компьютере).
Кстати, tbb действительно круто.
РЕДАКТИРОВАТЬ: Я был вдохновлен отличным решением Александру и заменил std :: sort на последнем этапе модифицированной версией его radix sort. Я использовал другой метод для работы с положительными / отрицательными числами, даже если ему нужно больше проходов через массив. Я также решил точно отсортировать массив и удалить сортировку вставки. Позже я потрачу некоторое время на тестирование того, как эти изменения влияют на производительность и, возможно, возвращают их. Однако с помощью радикальной сортировки время сократилось с ~ 2,35 до ~ 1,63 с.
источник
Без умного, просто чтобы обеспечить намного более быстрый наивный сортировщик, вот один в C, который должен быть в значительной степени эквивалентен вашему Python:
Скомпилировано с
gcc -O3
, на моей машине это занимает более минуты меньше, чем Python: около 11 с по сравнению с 87 с.источник
Я разделил на сегменты на основе стандартного отклонения, которое лучше всего разбить на 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.
Я использую Scala 2.9, для параллельной коллекции. Вы можете просто скачать дистрибутив tar.gz.
Для компиляции: scalac SortFile.scala (я просто скопировал его прямо в папку scala / bin.
Для запуска: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (я запустил его с 2 гигабайтами оперативной памяти и получил примерно в то же время)
Изменить: Удален allocateDirect, медленнее, чем просто распределить. Убрано заполнение начального размера для буферов массива. На самом деле сделал это прочитать все 50000000 значений. Переписано, чтобы избежать проблем с автобоксом (все еще медленнее, чем наивный c)
источник
Просто поместите это в файл cs и скомпилируйте его с помощью csc в теории: (Требуется моно)
источник
Поскольку вы знаете, что такое распределение, вы можете использовать прямую индексацию O (N). (Если вам интересно, что это такое, предположим, что у вас есть колода из 52 карт, и вы хотите ее отсортировать. Просто наберите 52 ячейки и бросьте каждую карточку в отдельную ячейку.)
У вас есть 5e7 дублей. Выделите массив результатов R из 5e7 double. Возьми каждый номер
x
и получиi = phi(x) * 5e7
. В основном делатьR[i] = x
. Иметь способ обработки коллизий, таких как перемещение числа, с которым он может столкнуться (как в простом хэш-кодировании). В качестве альтернативы, вы можете сделать R в несколько раз больше, заполнить уникальным пустым значением. В конце вы просто подметаете элементы R.phi
это просто гауссовская функция распределения. Он преобразует гауссово распределенное число между +/- бесконечностью в равномерное распределенное число между 0 и 1. Простой способ его вычисления - поиск в таблице и интерполяция.источник
Вот еще одно последовательное решение:
Я сомневаюсь, что это превосходит многопоточное решение, но время на моем ноутбуке i7 таково (stdsort - это решение C ++, представленное в другом ответе):
Обратите внимание, что это решение имеет линейную сложность по времени (потому что оно использует специальное представление двойников).
РЕДАКТИРОВАТЬ : Исправлен порядок увеличения элементов.
РЕДАКТИРОВАТЬ : Улучшена скорость почти на полсекунды.
РЕДАКТИРОВАТЬ : Улучшена скорость еще на 0,7 секунды. Сделал алгоритм более дружественным к кешу.
РЕДАКТИРОВАТЬ : Улучшена скорость еще на 1 секунду. Так как там только 50.000.000 элементов, я могу частично отсортировать мантиссу и использовать вставку сортировки (которая дружественна кешу) для исправления неуместных элементов. Эта идея удаляет около двух итераций из последнего цикла сортировки по основанию.
РЕДАКТИРОВАТЬ : 0,16 меньше секунд. Первый std :: reverse можно удалить, если порядок сортировки обратный.
источник
Взять решение Кристиана Аммера и распараллелить его с многопоточными блоками Intel
Если у вас есть доступ к библиотеке Intel Performance Primitives (IPP), вы можете использовать ее основную сортировку. Просто замени
с участием
а также
с участием
На моем двухъядерном ноутбуке время
источник
Как насчет реализации параллельной быстрой сортировки, которая выбирает свои основные значения на основе статистики распределения, тем самым обеспечивая разделы равного размера? Первый круг будет в среднем (в данном случае ноль), следующая пара будет в 25-м и 75-м процентилях (+/- -0,67449 стандартных отклонений) и т. Д., При этом каждый раздел делит оставшийся набор данных пополам вдвое больше или менее идеально.
источник
Очень некрасиво (зачем использовать массивы, когда я могу использовать переменные, заканчивающиеся цифрами), но быстрый код (моя первая попытка std :: threads), все время (реальное время) в моей системе 1,8 с (по сравнению с std :: sort () 4,8 с), скомпилировать с помощью g ++ -std = c ++ 0x -O3 -march = native -pthread Просто передать данные через стандартный ввод (работает только для 50M).
// Изменить, чтобы прочитать файл gaussian.dat.
источник
Решение на C ++, использующее
std::sort
(в конечном итоге быстрее, чем qsort, относительно производительности qsort vs std :: sort )Я не могу с уверенностью сказать, сколько времени это займет, потому что у меня есть только 1 ГБ на моей машине, и с данным кодом Python я мог бы сделать
gaussian.dat
файл только с двойными 25 млн. (Без ошибки памяти). Но мне очень интересно, как долго работает алгоритм std :: sort.источник
sort.h
файл, чтобы скомпилировать его с C ++. Это было примерно в два раза медленнее, чемstd::sort
. Не знаю почему, может быть из-за оптимизации компилятора?Вот смесь радикальной сортировки Александру и умного поворота Жарека. Скомпилируйте это с
Вы можете изменить размер радиуса, определив STEP (например, добавить -DSTEP = 11). Я нашел лучший для моего ноутбука 8 (по умолчанию).
По умолчанию он разбивает проблему на 4 части и запускает ее в нескольких потоках. Вы можете изменить это, передав параметр глубины в командную строку. Так что, если у вас есть два ядра, запустите его как
и если у вас есть 16 ядер
Максимальная глубина сейчас составляет 6 (64 потока). Если вы ставите слишком много уровней, вы просто замедляете код.
Одна вещь, которую я также попробовал, была сортировка radix из библиотеки Intel Performance Primitives (IPP). Реализация Александру явно затрагивает IPP, причем IPP работает примерно на 30% медленнее. Это изменение также включено сюда (закомментировано).
РЕДАКТИРОВАТЬ : я реализовал улучшения кэша Александру, и это сэкономило около 30% времени на моей машине.
РЕДАКТИРОВАТЬ : Это реализует рекурсивную сортировку, поэтому он должен хорошо работать на 16-ядерном компьютере Александру. Он также использует последнее улучшение Александру и удаляет одно из обратного. Для меня это дало улучшение на 20%.
РЕДАКТИРОВАТЬ : Исправлена ошибка со знаком, которая приводила к неэффективности при наличии более 2 ядер.
РЕДАКТИРОВАТЬ : Удалена лямбда, поэтому он будет компилироваться с более старыми версиями gcc. Он включает закомментированное изменение кода IPP. Я также исправил документацию для работы на 16 ядрах. Насколько я могу судить, это самая быстрая реализация.
РЕДАКТИРОВАТЬ : Исправлена ошибка, когда STEP не 8. Увеличено максимальное количество потоков до 64. Добавлена некоторая информация о времени.
источник
step
(11 на моем ноутбуке было оптимальным).int cnt[mask]
должно бытьint cnt[mask + 1]
. Для лучших результатов используйте фиксированное значениеint cnt[1 << 16]
.Я думаю, это действительно зависит от того, что вы хотите сделать. Если вы хотите отсортировать группу гауссов, то это вам не поможет. Но если вы хотите кучу отсортированных гауссиан, это будет. Даже если это немного не решит проблему, я думаю, что будет интересно сравнить ее с реальными процедурами сортировки.
Если хочешь, чтобы что-то было быстрым, делай меньше.
Вместо генерации группы случайных выборок из нормального распределения и последующей сортировки можно создать группу выборок из нормального распределения в отсортированном порядке.
Вы можете использовать решение здесь для создания п однородных случайных чисел в отсортированном порядке. Затем вы можете использовать обратный cdf (scipy.stats.norm.ppf) нормального распределения, чтобы превратить единообразные случайные числа в числа из нормального распределения посредством выборки обратного преобразования .
Если вы хотите сделать свои руки более грязными, я думаю, вы могли бы ускорить многие обратные вычисления cdf, используя какой-то итерационный метод и используя предыдущий результат в качестве первоначального предположения. Поскольку предположения будут очень близки, вероятно, одна итерация даст вам большую точность.
источник
Попробуйте изменить решение Guvante с помощью функции Main (), она начнет сортировку, как только будет выполнено чтение 1/4 ввода-вывода, и в моем тесте она будет быстрее:
источник
Поскольку вы знаете распределение, моя идея состоит в том, чтобы создать 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 хорошо.
источник
std::sort()
, но это намного медленнее, чем решение радиосортов Александру.Одна из низкоуровневых идей оптимизации состоит в том, чтобы поместить два двойных в регистр SSE, чтобы каждый поток работал с двумя элементами одновременно. Это может быть сложно сделать для некоторых алгоритмов.
Еще одна вещь, которую нужно сделать, это отсортировать массив в кеширующие части, а затем объединить результаты. Следует использовать два уровня: например, сначала 4 КБ для L1, затем 64 КБ для L2.
Это должно быть очень дружественным к кешу, так как сортировка сегментов не будет выходить за пределы кэша, а окончательное слияние будет последовательно обходить память.
В наши дни вычисления намного дешевле, чем доступ к памяти. Однако у нас есть большое количество элементов, поэтому трудно сказать, какой размер массива, когда глупая сортировка с поддержкой кэша выполняется медленнее, чем версия с низкой сложностью, не поддерживающая кэширование.
Но я не буду предоставлять реализацию вышеупомянутого, поскольку я сделал бы это в Windows (VC ++).
источник
Вот реализация сортировки с линейным сканированием. Я думаю, что это быстрее, чем все текущие однопоточные реализации, за исключением сортировки по основанию. Он должен иметь ожидаемое линейное время выполнения, если я достаточно точно оцениваю cdf (я использую линейную интерполяцию значений, которые я нашел в Интернете) и не допустил ошибок, которые могли бы вызвать чрезмерное сканирование:
источник
Я не знаю, почему я не могу отредактировать свой предыдущий пост, поэтому вот новая версия, на 0,2 секунды быстрее (но примерно на 1,5 с быстрее во время процессора (пользователь)). Это решение имеет 2 программы, которые предварительно рассчитывают квантили для нормального распределения для сортировки сегментов и сохраняют их в таблице, t [double * scale] = индекс сегмента, где scale - произвольное число, которое делает возможным удвоение приведения. Тогда основная программа может использовать эти данные, чтобы поместить двойники в правильное ведро. У этого есть один недостаток: если данные не гауссовы, они не будут работать правильно (а также почти нулевой шанс работать некорректно для нормального распределения), но модификация для особого случая проста и быстра (только количество проверок сегментов и падение до стандартного значения) ::Сортировать()).
Компиляция: g ++ => http://pastebin.com/WG7pZEzH вспомогательная программа
g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZP основная программа сортировки
источник
Вот еще одно последовательное решение. Этот использует тот факт, что элементы распределены нормально, и я думаю, что идея в целом применима для сортировки, близкой к линейному времени.
Алгоритм такой:
phi()
Функцию в реализации)size * phi(x)
К сожалению, скрытая константа довольно велика, и это решение в два раза медленнее алгоритма сортировки по основанию.
источник
Мой личный фаворит с использованием многопоточных строительных блоков Intel уже был опубликован, но вот грубое параллельное решение с использованием JDK 7 и его нового API-интерфейса fork / join:
Важный отказ от ответственности : я использовал адаптацию быстрой сортировки для fork / join: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
Для этого вам понадобится бета-версия JDK 7 (http://jdk7.java.net/download.html).
На моем 2.93 ГГц Quad Core i7 (OS X):
Python ссылка
Java JDK 7 форк / соединение
Я также попытался поэкспериментировать с параллельным чтением и преобразованием байтов в удвоения, но я не увидел там никакой разницы.
Обновить:
Если кто-то хочет поэкспериментировать с параллельной загрузкой данных, версия с параллельной загрузкой приведена ниже. Теоретически это может сделать его еще немного быстрее, если ваше устройство ввода-вывода имеет достаточную параллельную емкость (обычно это делают SSD). Существуют также некоторые издержки при создании дублей из байтов, так что они потенциально могут работать быстрее параллельно. На моих системах (Ubuntu 10.10 / Nehalem Quad / Intel X25M SSD и OS X 10.6 / i7 Quad / Samsung SSD) я не увидел никакой реальной разницы.
Update2:
Я выполнил код на одной из наших 12-ядерных машин с небольшой модификацией, чтобы установить фиксированное количество ядер. Это дало следующие результаты:
В этой системе я также попробовал версию Python, которая занимала 1m2.994s, и версию Cjjarek C ++, которая занимала 1.925s (почему-то версия Zjarek C ++, кажется, работает относительно быстрее на компьютере static_rtti).
Я также попробовал, что случилось, если я удвоил размер файла до 100 000 000 удваивается:
В этом случае версия Zjarek для C ++ заняла 3.968 с. Питон просто занял слишком много времени здесь.
150 000 000 пар:
В этом случае версия Zjarek's C ++ была 6.044s. Я даже не пытался Python.
Версия C ++ очень согласуется с ее результатами, где Java немного качается. Сначала она становится немного более эффективной, когда проблема становится больше, но затем снова становится менее эффективной.
источник
Версия с использованием традиционных pthreads. Код для слияния скопирован из ответа Гуванте. Компилировать с
g++ -O3 -pthread
.На моем ноутбуке я получаю следующие результаты:
источник
Вот последовательная реализация C99, которая пытается реально использовать известный дистрибутив. Он в основном выполняет один раунд сортировки сегментов с использованием информации о распределении, затем несколько циклов быстрой сортировки в каждом сегменте, предполагая равномерное распределение в пределах сегмента, и, наконец, измененную сортировку выбора для копирования данных обратно в исходный буфер. Быстрая сортировка запоминает точки разделения, поэтому сортировка выбора должна работать только с небольшими порциями. И несмотря на (потому что?) Всю эту сложность, это даже не очень быстро.
Чтобы сделать оценку Φ быстрой, значения выбираются в нескольких точках, а позже используется только линейная интерполяция. На самом деле не имеет значения, точно ли оценивается Φ, если приближение строго монотонно.
Размеры бункера выбираются таким образом, чтобы вероятность переполнения бункера была незначительной. Точнее, с текущими параметрами вероятность того, что набор данных из 50000000 элементов вызовет переполнение ячейки, равна 3.65e-09. (Это может быть вычислено с использованием функции выживания в распределении Пуассона ) .
Для компиляции, пожалуйста, используйте
Поскольку вычислений значительно больше, чем в других решениях, эти флаги компилятора необходимы для того, чтобы сделать его хотя бы достаточно быстрым. Без
-msse3
преобразований,double
чтобыint
стать очень медленно. Если ваша архитектура не поддерживает SSE3, эти преобразования также можно выполнить с помощьюlrint()
функции.Код довольно уродливый - не уверен, что он соответствует требованию «разумно читаемого» ...
источник
Это использует erf () для правильного размещения каждого элемента в ячейке, а затем сортирует каждую ячейку. Он сохраняет массив полностью на месте.
Первый проход: docensus () подсчитывает количество элементов в каждом бине.
Второй проход: partition () переставляет массив, помещая каждый элемент в соответствующую корзину
Третий этап: sortbins () выполняет qsort для каждого бина.
Это наивно и вызывает дорогую функцию erf () дважды для каждого значения. Первый и третий проходы потенциально распараллеливаются. Второй является очень последовательным и, вероятно, замедлен из-за его очень случайных образцов доступа к памяти. Также может быть целесообразно кэшировать номер каждой двойной ячейки, в зависимости от соотношения мощности процессора и скорости памяти.
Эта программа позволяет вам выбрать количество корзин для использования. Просто добавьте второй номер в командной строке. Я скомпилировал его с помощью gcc -O3, но моя машина настолько слабая, что не могу сказать вам хороших показателей производительности.
Редактировать: Пуф! Моя программа на C волшебным образом превратилась в программу на C ++ с использованием std :: sort!
источник
Взгляните на реализацию сортировки по основанию от Michael Herf ( Radix Tricks ). На моей машине сортировка была в 5 раз быстрее по сравнению с
std::sort
алгоритмом в моем первом ответе. Имя функции сортировки являетсяRadixSort11
.источник