Самый быстрый способ отсортировать 10 чисел? (числа 32-битные)

Я решаю проблему, и она включает в себя очень быструю сортировку 10 чисел (int32). Моему приложению нужно как можно быстрее отсортировать 10 чисел в миллионы раз. Я отбираю набор данных из миллиардов элементов, и каждый раз мне нужно выбрать из него 10 чисел (упрощенно) и отсортировать их (и сделать выводы из отсортированного списка из 10 элементов).

В настоящее время я использую сортировку вставкой, но полагаю, что смогу реализовать очень быстрый пользовательский алгоритм сортировки для моей конкретной проблемы с 10 числами, которые превзошли бы сортировку вставкой.

Как я могу подойти к этой проблеме?


person bodacydo    schedule 23.08.2015    source источник
comment
Как бы грубо это ни звучало, серия вложенных операторов if должна работать лучше всего. Избегайте петель.   -  person John Alexiou    schedule 24.08.2015
comment
Для небольших наборов сортировка вставкой и сортировка по выбору работают на удивление хорошо. Другой вариант - это поразрядная сортировка, поскольку не может быть более 10 различных значений. Избегайте вызовов функций.   -  person wildplasser    schedule 24.08.2015
comment
Ожидаете ли вы, что числа будут выдаваться вам с каким-либо отклонением в наборе перестановок, или они будут распределены равномерно? Будет ли какая-либо связь между порядком следования одного списка и следующего?   -  person Douglas Zare    schedule 24.08.2015
comment
Весь набор данных (с миллиардами чисел) распределяется в соответствии с законом Бенфорда, но когда я случайным образом выбираю элементы из этого набора, их уже нет (я думаю).   -  person bodacydo    schedule 24.08.2015
comment
Целые числа со знаком или без знака? (или даже если целые числа подписаны, можете ли вы гарантировать, что сохраненные значения будут положительными?)   -  person IQAndreas    schedule 24.08.2015
comment
Вы можете прочитать этот stackoverflow.com/q/2786899/995714   -  person phuclv    schedule 24.08.2015
comment
Если вы выбираете случайным образом из миллиардов элементов, то вполне возможно, что задержка для извлечения этих данных может иметь большее влияние, чем время, необходимое для сортировки выбранных элементов, даже если весь набор данных находится в ОЗУ. Вы можете проверить влияние, сравнив производительность, выбирая данные последовательно или случайным образом.   -  person Steve S.    schedule 24.08.2015
comment
@SteveS .: верно, звучит так, будто OP должен сначала выполнить один проход с частичным перемешиванием. Отбор проб без замены будет более эффективным и более эффективным.   -  person smci    schedule 25.08.2015
comment
@ LưuVĩnhPhúc предоставил очень хорошую ссылку, кажется, вам следует использовать порядок ранжирования.   -  person jxh    schedule 25.08.2015
comment
@IQAndreas Иногда подписано, иногда без подписи. Лучше всего считать беззнаковым.   -  person bodacydo    schedule 25.08.2015
comment
@jxh Ой, я не заметил там того нового ответа ...   -  person m69 ''snarky and unwelcoming''    schedule 01.09.2015
comment
@ m69: Это сбивает с толку. В последней модификации вопроса автора говорится, что порядок ранжирования самый быстрый, но необработанные временные данные показывают, что модифицированный порядок свопов для сортировочной сети выполняется быстрее.   -  person jxh    schedule 01.09.2015
comment
@bodacydo Не могли бы вы сообщить нам, какой вариант вы в конечном итоге использовали, и как и почему вы его выбрали, и, возможно, добавить результаты тестов, если они у вас есть?   -  person m69 ''snarky and unwelcoming''    schedule 08.09.2015


Ответы (11)


(Следуя предложению HelloWorld изучить сети сортировки.)

Кажется, что сеть с 29 сравнениями / обменом - это самый быстрый способ выполнить сортировку с 10 входами. Я использовал сеть, открытую Ваксманом в 1969 году для этого примера в JavaScript, который должен переводиться непосредственно на C, поскольку это просто список if операторов, сравнений и свопов.

function sortNet10(data) {    // ten-input sorting network by Waksman, 1969
    var swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
    return(data);
}

alert(sortNet10([5,7,1,8,4,3,6,9,2,0]));

Вот графическое представление сети, разделенной на независимые фазы.

Сортировочная сеть с 10 входами (Ваксман, 1969)

Чтобы воспользоваться преимуществами параллельной обработки, группировку 5-4-3-4-4-4-3-2 можно изменить на группировку 4-4-4-4-4-4-3-2.

Сортировочная сеть с 10 входами (Ваксман, 1969) перегруппирована

person m69 ''snarky and unwelcoming''    schedule 24.08.2015
comment
предположение; используйте макрос подкачки. как #define SORTPAIR(data, i1, i2) if (data[i1] > data[i2]) { int swap = data[i1]... } - person Peter Cordes; 24.08.2015
comment
Можно ли логически показать, что это минимум? - person corsiKa; 24.08.2015
comment
@corsiKa Да, сортировочные сети были областью исследований с первых дней информатики. Во многих случаях оптимальные решения известны десятилетиями. См. en.wikipedia.org/wiki/Sorting_network. - person m69 ''snarky and unwelcoming''; 24.08.2015
comment
Я сделал Jsperf для тестирования и могу подтвердить, что сетевая сортировка более чем в 20 раз быстрее, чем собственная сортировка браузеров. jsperf.com/fastest-10-number-sort - person Daniel; 24.08.2015
comment
Я не эксперт, но, может быть, побитовая замена XOR была бы еще быстрее? Я обычно использую эту технику в javascript, я не знаю, верно ли это и в c. Что-то вроде: var1^=var2, var2^=var1, var1^=var2 - person Katai; 24.08.2015
comment
@Katai Это разрушит любую оптимизацию, которую может произвести ваш компилятор. Плохая идея. Прочтите это для получения дополнительной информации. en.wikipedia.org/wiki/ - person Antzi; 24.08.2015
comment
Простой способ убедиться, что компилятор C произвел правильно оптимизированный своп, - это сгенерировать файл сборки .S (опция gcc -S) и выполнить поиск мнемоники bswap *. - person dargaud; 24.08.2015
comment
В C / C ++ дальнейшее улучшение скорости, вероятно, может быть достигнуто путем выравнивания всех данных в одних и тех же 64 байтах, то есть в одной строке кэша (stackoverflow.com/questions/14707803/) - person Peter; 24.08.2015
comment
OP запрашивает C / C ++, и мы проверяем это с помощью теста JS. Ой. Если компилятор переведет это в CAS / CMPXCHG, это может быть самым быстрым решением. Если он окажется CMP + Jcc - неудачные предсказания ветвления снизят производительность. Если вам нужна скорость, подумайте о самостоятельной векторизации. Больше (в кэше / регистрах) свопов может быть быстрее, чем большее количество (неверно предсказанных) ветвей. Как всегда - ›профиль / тест. - person Andreas; 28.08.2015
comment
Вы можете попробовать использовать безотказную версию swap-if-большего, если ваши числа int32: c = ((b-a) & (b-a)>>31); a += c; b -= c; вместо if (a > b) { swap ... } - person e.tadeu; 31.08.2015
comment
С SSE вы можете делать свопы с MIN / MAX - person Ben Jackson; 01.09.2015
comment
@dargaud: BSWAP меняет местами порядок байтов, и XCHG не обязательно быстрее, чем правильно упорядоченные MOV - person peterchen; 01.09.2015

Когда вы имеете дело с этим фиксированным размером, обратите внимание на сети сортировки. Эти алгоритмы имеют фиксированное время выполнения и не зависят от их входных данных. Для вашего варианта использования у вас нет таких накладных расходов, как у некоторых алгоритмов сортировки.

Bitonic sort является реализацией такой сети. Этот лучше всего работает с len (n) ‹= 32 на CPU. На больших входах вы можете подумать о переходе на графический процессор.

Кстати, здесь есть хорошая страница для сравнения алгоритмов сортировки (хотя на ней отсутствует bitonic sort):

Анимация алгоритмов сортировки

person HelloWorld    schedule 23.08.2015
comment
Прекрасное решение. Я нашел следующие пары: {{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {1, 3}, {2, 4}, { 5, 7}, {6, 8}, {1, 9}, {2, 7}, {8, 10}, {1, 5}, {3, 8}, {4, 10}, {6, 9}, {2, 6}, {3, 5}, {4, 9}, {7, 8}, {2, 3}, {4, 7}, {5, 6}, {8, 9} , {3, 5}, {4, 6}, {7, 8}, {4, 5}, {6, 7}} в mathpuzzle.com/25Feb.htm. Я не проверял их точность. - person Erick G. Hagstrom; 24.08.2015
comment
@ ErickG.Hagstrom Есть много решений; пока они используют 29 сравнений, они одинаково эффективны. Я использовал решение Ваксмана 1969 года; он, по-видимому, был первым, кто обнаружил версию с 29 сравнениями. - person m69 ''snarky and unwelcoming''; 24.08.2015
comment
Да, @ m69. Их больше миллиона. Решение Ваксмана имеет длину 29 и глубину 9. Связанное мной решение является усовершенствованием по сравнению с измерением глубины: длина = 29, глубина = 8. Конечно, при реализации на C глубина не имеет значения. - person Erick G. Hagstrom; 24.08.2015
comment
@ ErickG.Hagstrom По-видимому, существует 87 решений с глубиной 7, первое из которых было найдено Кнутом в 1973 году, но я не смог найти ни одного из них с помощью быстрого поиска в Google. larc.unt.edu/ian/pubs/9-input.pdf (см. Заключение, стр.14) - person m69 ''snarky and unwelcoming''; 24.08.2015
comment
Спасибо @ m69. Это гораздо более авторитетный источник, чем тот, на который я наткнулся. 7, и было доказано (исчерпывающим поиском) оптимальным. - person Erick G. Hagstrom; 24.08.2015
comment
@ ErickG.Hagstrom: глубина может не иметь значения на уровне C, но, предположительно, как только компилятор и ЦП закончат с ней, есть некоторая вероятность, что она будет частично распараллелена внутри ЦП, и поэтому меньшая глубина может помочь. Конечно, в зависимости от процессора: некоторые процессоры относительно просты и выполняют одно действие за другим, тогда как некоторые процессоры могут выполнять несколько операций в полете, в частности, вы можете получить очень разную производительность для любых нагрузок и сохранений в стеке, которые необходимы в чтобы управлять 10 переменными, в зависимости от того, как они сделаны. - person Steve Jessop; 24.08.2015
comment
@ ErickG.Hagstrom Из статьи Яна Парберри не сразу ясно, но сети глубины 7 имеют длину больше 29. См. Knuth, The Art Of Computer Programming Vol.III, §5.3.4, рис. 49 и 51. - person m69 ''snarky and unwelcoming''; 24.08.2015
comment
@ m69 Ой! Тогда стоит ли нам выбирать длину = 29 или глубину = 7, но не то и другое вместе? (У меня нет доступа к Knuth.) - person Erick G. Hagstrom; 24.08.2015
comment
@ ErickG.Hagstrom Да, у Кнута 31 год. А поскольку Парберри мимоходом упоминает только 87 сетей с глубиной 7, я предполагаю, что ни одна из них не была короче 31 Кнута. Так что, если опубликованная вами сеть с глубиной 8 подтверждается, вероятно, это текущая длина -29 чемпион. - person m69 ''snarky and unwelcoming''; 24.08.2015
comment
Между прочим, извиняюсь за то, что сбежал с вашей идеей, и сто голосов "за". - person m69 ''snarky and unwelcoming''; 22.09.2015
comment
Без проблем! Вы действительно хорошо поработали! Вы заслуживаете голосов :) - person HelloWorld; 22.09.2015
comment
Для статической визуализации существует битоническая сортировка в sortvis - person greybeard; 22.09.2015

Используйте сеть сортировки, которая имеет сравнения в группах по 4, так что вы можете делать это в регистрах SIMD. Пара упакованных инструкций min / max реализует функцию упакованного компаратора. Извините, у меня сейчас нет времени искать страницу, которую я помню об этом, но, надеюсь, поиск в сетях сортировки SIMD или SSE что-то даст.

x86 SSE действительно имеет упакованные 32-битные целочисленные инструкции min и max для векторов из четырех 32-битных целых чисел. AVX2 (Haswell и более поздние версии) имеют то же самое, но для векторов 256b по 8 int. Есть также эффективные инструкции перемешивания.

Если у вас много независимых небольших сортировок, возможно, можно будет выполнить 4 или 8 сортировок параллельно с использованием векторов. Esp. если вы выбираете элементы случайным образом (так что данные для сортировки в любом случае не будут непрерывными в памяти), вы можете избежать перемешивания и просто сравнить в нужном вам порядке. 10 регистров для хранения всех данных из 4 (AVX2: 8) списков по 10 целых чисел по-прежнему оставляют 6 регистров для временного пространства.

Сети векторной сортировки менее эффективны, если вам также необходимо отсортировать связанные данные. В этом случае наиболее эффективным способом, по-видимому, является использование упакованного сравнения, чтобы получить маску измененных элементов, и использовать эту маску для смешивания векторов (ссылок на) связанных данных.

person Peter Cordes    schedule 24.08.2015

А как насчет развернутой сортировки без ветвей?

#include <iostream>
#include <algorithm>
#include <random>

//return the index of the minimum element in array a
int min(const int * const a) {
  int m = a[0];
  int indx = 0;
  #define TEST(i) (m > a[i]) && (m = a[i], indx = i ); 
  //see http://stackoverflow.com/a/7074042/2140449
  TEST(1);
  TEST(2);
  TEST(3);
  TEST(4);
  TEST(5);
  TEST(6);
  TEST(7);
  TEST(8);
  TEST(9);
  #undef TEST
  return indx;
}

void sort( int * const a ){
  int work[10];
  int indx;
  #define GET(i) indx = min(a); work[i] = a[indx]; a[indx] = 2147483647; 
  //get the minimum, copy it to work and set it at max_int in a
  GET(0);
  GET(1);
  GET(2);
  GET(3);
  GET(4);
  GET(5);
  GET(6);
  GET(7);
  GET(8);
  GET(9);
  #undef GET
  #define COPY(i) a[i] = work[i];
  //copy back to a
  COPY(0);
  COPY(1);
  COPY(2);
  COPY(3);
  COPY(4);
  COPY(5);
  COPY(6);
  COPY(7);
  COPY(8);
  COPY(9);
  #undef COPY
}

int main() {
  //generating and printing a random array
  int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
  std::random_device rd;
  std::mt19937 g(rd());
  std::shuffle( a, a+10, g);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  }
  std::cout << std::endl;

  //sorting and printing again
  sort(a);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  } 

  return 0;
}

http://coliru.stacked-crooked.com/a/71e18bc4f7fa18c6

Единственные релевантные строки - это первые две #define.

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


Контрольный показатель

Я сравнил с сетью сортировки, и мой код кажется медленнее. Однако я попытался удалить развертку и копию. Запуск этого кода:

#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>

int min(const int * const a, int i) {
  int m = a[i];
  int indx = i++;
  for ( ; i<10; i++) 
    //see http://stackoverflow.com/a/7074042/2140449
    (m > a[i]) && (m = a[i], indx = i ); 
  return indx;
}

void sort( int * const a ){
  for (int i = 0; i<9; i++)
    std::swap(a[i], a[min(a,i)]); //search only forward
}


void sortNet10(int * const data) {  // ten-input sorting network by Waksman, 1969
    int swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
}


std::chrono::duration<double> benchmark( void(*func)(int * const), const int seed ) {
  std::mt19937 g(seed);
  int a[10] = {10,11,12,13,14,15,16,17,18,19};
  std::chrono::high_resolution_clock::time_point t1, t2; 
  t1 = std::chrono::high_resolution_clock::now();
  for (long i = 0; i < 1e7; i++) {
    std::shuffle( a, a+10, g);
    func(a);
  }
  t2 = std::chrono::high_resolution_clock::now();
  return std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
}

int main() {
  std::random_device rd;
  for (int i = 0; i < 10; i++) {
    const int seed = rd();
    std::cout << "seed = " << seed << std::endl;
    std::cout << "sortNet10: " << benchmark(sortNet10, seed).count() << std::endl;
    std::cout << "sort:      " << benchmark(sort,      seed).count() << std::endl;
  }
  return 0;
}

Я постоянно получаю лучший результат для сортировки без ответвлений по сравнению с сортировочной сетью.

$ gcc -v
gcc version 5.2.0 (GCC) 
$ g++ -std=c++11 -Ofast sort.cpp && ./a.out
seed = -1727396418
sortNet10: 2.24137
sort:      2.21828
seed = 2003959850
sortNet10: 2.23914
sort:      2.21641
seed = 1994540383
sortNet10: 2.23782
sort:      2.21778
seed = 1258259982
sortNet10: 2.25199
sort:      2.21801
seed = 1821086932
sortNet10: 2.25535
sort:      2.2173
seed = 412262735
sortNet10: 2.24489
sort:      2.21776
seed = 1059795817
sortNet10: 2.29226
sort:      2.21777
seed = -188551272
sortNet10: 2.23803
sort:      2.22996
seed = 1043757247
sortNet10: 2.2503
sort:      2.23604
seed = -268332483
sortNet10: 2.24455
sort:      2.24304
person DarioP    schedule 24.08.2015
comment
Результаты не очень впечатляющие, но на самом деле то, что я ожидал. Сортировочная сеть сводит к минимуму сравнения, а не обмен. Когда все значения уже находятся в кэше, сравнения намного дешевле, чем свопы, поэтому сортировка выбора (которая минимизирует количество свопов) имеет преимущество. (и сравнений не так уж и много: сеть с 29 сравнениями, до 29 свопов ?; против сортировки по выбору с 45 сравнениями и максимум 9 свопами) - person example; 24.08.2015
comment
Да, и у него есть ветки - если только строка for ( ; i<10; i++) (m > a[i]) && (m = a[i], indx = i ); не оптимизирована исключительно хорошо. (короткое замыкание обычно является формой разветвления) - person example; 24.08.2015
comment
@EugeneRyabtsev это тоже, но он все время получает одни и те же случайные последовательности, поэтому он должен отменить. Я пробовал поменять std::shuffle на for (int n = 0; n<10; n++) a[n]=g();. Время выполнения сокращено вдвое, и сеть теперь работает быстрее. - person DarioP; 25.08.2015
comment
Как это сравнить с std::sort в libc ++? - person gnzlbg; 01.09.2015
comment
@gnzlbg Я тоже пробовал std::sort, но он работал так плохо, что я даже не включил его в тест. Я предполагаю, что с крошечными наборами данных есть довольно накладные расходы. - person DarioP; 01.09.2015
comment
@example: я ожидал, что если все находится в кеше, свопы будут дешевыми, а сравнения дорогими, потому что в кэше L1 доступ к данным дешевый, тогда как штраф за неправильное прогнозирование ветки не затронут. Вы уверены, что это наоборот, как вы утверждаете в своем комментарии? А если не опечатка - почему? - person Eamon Nerbonne; 01.09.2015
comment
@DarioP спасибо, я думаю, что даже если он плохо работает, стоит его включить. В основном это показывает, что зайти так далеко стоит. - person gnzlbg; 01.09.2015
comment
@EamonNerbonne, я думал, что сравнения можно проводить без повторного взаимодействия с оперативной памятью, в то время как записи (например, свопы) должны быть записаны рано или поздно. Даже если они записываются только тогда, когда данные покидают кеш (в чем я не уверен), ЦП, скорее всего, запишет все данные, если все они были изменены в какой-то момент - в то время как сортировка вставки могла найти один обмен это было действительно необходимо. По общему признанию, я больше не уверен в этом аргументе, но я все же сказал бы, что свопы должны быть более обширными, чем сравнения. - person example; 02.09.2015
comment
Да, и то же самое касается кеша для регистрации связи. Если он только читается и сравнивается, его не нужно обновлять в кеше. - person example; 02.09.2015
comment
@example: не каждый своп достигает основной памяти. Поскольку данные плотные и маленькие (всего 40 байт, то есть менее 1 строки кеша), большая часть этих записей будет происходить в кеш, который не будет сбрасываться при каждой записи. Не должно иметь большого значения, будете ли вы менять местами несколько раз или десятки раз - должна произойти только одна запись в основную память (или две, если она смещена). - person Eamon Nerbonne; 02.09.2015
comment
Вы пытались протестировать сеть сортировки с помощью трюка с подкачкой из этого ответа? На моем компьютере он дает результаты в три раза быстрее, что согласуется с утверждениями, сделанными в исследовательской работе по этому вопросу. - person Morwenn; 30.10.2015
comment
Есть ли шанс, что вы можете добавить сравнение для моего алгоритма ниже? (сортировка развернутой вставки) - person Glenn Teitelbaum; 25.05.2016

Вопрос не говорит о том, что это какое-то веб-приложение. Мое внимание привлекла одна вещь:

Я отбираю набор данных из миллиардов элементов, и каждый раз мне нужно выбрать из него 10 чисел (упрощенно) и отсортировать их (и сделать выводы из отсортированного списка из 10 элементов).

Мне, как инженеру по программному и аппаратному обеспечению, совершенно не нравится FPGA. Я не знаю, какие выводы вам нужно сделать из отсортированного набора чисел или откуда берутся данные, но я знаю, что было бы почти тривиально обработать где-то между сотней миллионов и миллиардом этих операций сортировки и анализа в секунду. Раньше я занимался секвенированием ДНК с помощью ПЛИС. Почти невозможно превзойти огромную вычислительную мощность ПЛИС, когда проблема хорошо подходит для такого типа решения.

На определенном уровне единственным ограничивающим фактором становится скорость загрузки данных в FPGA и скорость их вывода.

В качестве ориентира я разработал высокопроизводительный процессор обработки изображений в реальном времени, который получал 32-битные данные изображения RGB со скоростью около 300 миллионов пикселей в секунду. Данные передаются через КИХ-фильтры, матричные умножители, таблицы поиска, блоки обнаружения пространственных границ и ряд других операций, прежде чем выйти на другой конец. И все это на относительно небольшой ПЛИС Xilinx Virtex2 с внутренней тактовой частотой примерно от 33 МГц до, если я правильно помню, 400 МГц. О да, он также имел реализацию контроллера DDR2 и работал с двумя банками памяти DDR2.

FPGA может выводить что-то вроде десяти 32-битных чисел при каждом тактовом переходе, работая на сотнях МГц. В начале операции будет небольшая задержка, поскольку данные заполняют конвейер / ы обработки. После этого вы сможете получать один результат за часы. Или даже больше, если обработку можно распараллелить путем репликации конвейера сортировки и анализа. Решение, в принципе, почти тривиальное.

Дело в том, что если приложение не привязано к ПК, а поток и обработка данных совместимы с решением FPGA (либо автономным, либо в качестве сопроцессорной карты в машине), вы никак не собираетесь может превзойти достижимый уровень производительности с программным обеспечением, написанным на любом языке, независимо от алгоритма.

Я просто провел быстрый поиск и нашел статью, которая может быть вам полезна. Похоже, он датируется 2012 годом. Вы можете намного повысить производительность сегодня (и даже тогда). Вот:

Сортировочные сети на ПЛИС

person martin's    schedule 28.08.2015

Недавно я написал небольшой класс, который использует алгоритм Бозе-Нельсона для создания сети сортировки при компиляции. время.

Его можно использовать для очень быстрой сортировки по 10 числам.

/**
 * A Functor class to create a sort for fixed sized arrays/containers with a
 * compile time generated Bose-Nelson sorting network.
 * \tparam NumElements  The number of elements in the array or container to sort.
 * \tparam T            The element type.
 * \tparam Compare      A comparator functor class that returns true if lhs < rhs.
 */
template <unsigned NumElements, class Compare = void> class StaticSort
{
    template <class A, class C> struct Swap
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            T t = Compare()(v0, v1) ? v0 : v1; // Min
            v1 = Compare()(v0, v1) ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A> struct Swap <A, void>
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            // Explicitly code out the Min and Max to nudge the compiler
            // to generate branchless code.
            T t = v0 < v1 ? v0 : v1; // Min
            v1 = v0 < v1 ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A, class C, int I, int J, int X, int Y> struct PB
    {
        inline PB(A &a)
        {
            enum { L = X >> 1, M = (X & 1 ? Y : Y + 1) >> 1, IAddL = I + L, XSubL = X - L };
            PB<A, C, I, J, L, M> p0(a);
            PB<A, C, IAddL, J + M, XSubL, Y - M> p1(a);
            PB<A, C, IAddL, J, XSubL, M> p2(a);
        }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 1>
    {
        inline PB(A &a) { Swap<A, C> s(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 2>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J); Swap<A, C> s1(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 2, 1>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J - 1); Swap<A, C> s1(a, I, J - 1); }
    };

    template <class A, class C, int I, int M, bool Stop = false> struct PS
    {
        inline PS(A &a)
        {
            enum { L = M >> 1, IAddL = I + L, MSubL = M - L};
            PS<A, C, I, L, (L <= 1)> ps0(a);
            PS<A, C, IAddL, MSubL, (MSubL <= 1)> ps1(a);
            PB<A, C, I, IAddL, L, MSubL> pb(a);
        }
    };

    template <class A, class C, int I, int M> struct PS <A, C, I, M, true>
    {
        inline PS(A &a) {}
    };

public:
    /**
     * Sorts the array/container arr.
     * \param  arr  The array/container to be sorted.
     */
    template <class Container> inline void operator() (Container &arr) const
    {
        PS<Container, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };

    /**
     * Sorts the array arr.
     * \param  arr  The array to be sorted.
     */
    template <class T> inline void operator() (T *arr) const
    {
        PS<T*, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };
};

#include <iostream>
#include <vector>

int main(int argc, const char * argv[])
{
    enum { NumValues = 10 };

    // Arrays
    {
        int rands[NumValues];
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    std::cout << "\n";

    // STL Vector
    {
        std::vector<int> rands(NumValues);
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    return 0;
}

Обратите внимание, что вместо оператора if (compare) swap мы явно кодируем тернарные операторы для min и max. Это помогает подтолкнуть компилятор к использованию автономного кода.

## Контрольные показатели

Следующие тесты скомпилированы с clang -O3 и выполнены на моем MacBook Air середины 2012 года.

### Сортировка случайных данных

Сравнивая его с кодом DarioP, вот количество миллисекунд, необходимое для сортировки 1 миллиона 32-битных массивов int размером 10:

Жестко заданная сортировка 10: 88,774 мс Шаблонная сортировка Бозе-Нельсона 10: 27,815 мс

Используя этот шаблонный подход, мы также можем генерировать сети сортировки во время компиляции для другого количества элементов.

Время (в миллисекундах) на сортировку 1 миллиона массивов различных размеров.

Количество миллисекунд для массивов размером 2, 4, 8 составляет 1,943, 8,655, 20,246 соответственно.

Шаблон времени статической сортировки Бозе-Нельсона C ++

Кредиты Гленну Тейтельбауму за сортировку развернутой вставкой.

Вот средние часы на сортировку для небольших массивов из 6 элементов. Код теста и примеры можно найти по этому вопросу:

Самый быстрый вид массива int фиксированной длины 6

Direct call to qsort library function       : 326.81
Naive implementation (insertion sort)       : 132.98
Insertion Sort (Daniel Stutzbach)           : 104.04
Insertion Sort Unrolled                     : 99.64
Insertion Sort Unrolled (Glenn Teitelbaum)  : 81.55
Rank Order                                  : 44.01
Rank Order with registers                   : 42.40
Sorting Networks (Daniel Stutzbach)         : 88.06
Sorting Networks (Paul R)                   : 31.64
Sorting Networks 12 with Fast Swap          : 29.68
Sorting Networks 12 reordered Swap          : 28.61
Reordered Sorting Network w/ fast swap      : 24.63
Templated Sorting Network (this class)      : 25.37

Он работает так же быстро, как самый быстрый пример в вопросе для 6 элементов.

### Производительность сортировки отсортированных данных

Часто входные массивы могут быть уже отсортированы или в основном отсортированы. В таких случаях лучшим выбором может быть сортировка вставкой.

Введите здесь описание изображения

Вы можете выбрать подходящий алгоритм сортировки в зависимости от данных.

Код, используемый для тестов, можно найти здесь.

person Vectorized    schedule 24.03.2016
comment
Есть ли шанс, что вы можете добавить сравнение для моего алгоритма ниже? - person Glenn Teitelbaum; 25.05.2016
comment
@GlennTeitelbaum, есть ли шанс, что вы добавили это в свои тесты и раскрыли средства и результаты? - person greybeard; 25.05.2016
comment
Престижность за добавление данных о сортировке отсортированного ввода. - person greybeard; 25.05.2016
comment
В некоторых системах v1 = v0 < v1 ? v1 : v0; // Max все еще может ветвиться, в этом случае его можно заменить на v1 += v0 - t, потому что если t равно v0, то v1 + v0 -t == v1 + v0 - v0 == v1, иначе t равно v1 и v1 + v0 -t == v1 + v0 - v1 == v0 - person Glenn Teitelbaum; 25.05.2016
comment
Тернар обычно компилируется в maxss или minss инструкцию на современных компиляторах. Но в тех случаях, когда это не сработает, можно использовать другие способы подкачки. :) - person Vectorized; 25.05.2016
comment
У вас есть ссылка на алгоритм Бозе-Нельсона? Боз, Р. К. и Нельсон, Р. Дж. 1962. Проблема сортировки. JACM, Vol. 9. Стр. 282-296.? Поисковые системы отдают предпочтение конденсации Бозе-Эйнштейна и наушникам Бозе. - person Peter Mortensen; 11.07.2020

Хотя сетевая сортировка имеет хорошие шансы быть быстрой на небольших массивах, иногда вы не можете превзойти сортировку вставкой при правильной оптимизации. Например, пакетная вставка с 2 элементами:

{
    final int a=in[0]<in[1]?in[0]:in[1];
    final int b=in[0]<in[1]?in[1]:in[0];
    in[0]=a;
    in[1]=b;
}
for(int x=2;x<10;x+=2)
{
    final int a=in[x]<in[x+1]?in[x]:in[x+1];
    final int b=in[x]<in[x+1]?in[x+1]:in[x];
    int y= x-1;

    while(y>=0&&in[y]>b)
    {
        in[y+2]= in[y];
        --y;
    }
    in[y+2]=b;
    while(y>=0&&in[y]>a)
    {
        in[y+1]= in[y];
        --y;
    }
    in[y+1]=a;
}
person warren    schedule 25.08.2015

Вы можете полностью развернуть сортировку вставкой.

Чтобы упростить эту задачу, можно использовать рекурсивные шаблоны без дополнительных функций. Поскольку это уже шаблон, int также может быть параметром шаблона. Это также упрощает создание массивов кодирования с размерами, отличными от 10.

Обратите внимание, что для сортировки int x[10] используется вызов insert_sort<int, 9>::sort(x);, поскольку класс использует индекс последнего элемента. Это можно было бы обернуть, но это было бы больше кода для чтения.

template <class T, int NUM>
class insert_sort;

template <class T>
class insert_sort<T,0>
// Stop template recursion
// Sorting one item is a no operation 
{
public:
    static void place(T *x) {}
    static void sort(T * x) {}
};

template <class T, int NUM>
class insert_sort
// Use template recursion to do insertion sort.
// NUM is the index of the last item, e.g. for x[10] call <9>
{
public:
    static void place(T *x)
    {
        T t1=x[NUM-1];
        T t2=x[NUM];
        if (t1 > t2)
        {
            x[NUM-1]=t2;
            x[NUM]=t1;
            insert_sort<T,NUM-1>::place(x);
        }
    }
    static void sort(T * x)
    {
        insert_sort<T,NUM-1>::sort(x); // Sort everything before
        place(x);                    // Put this item in
    }
};

В моем тестировании это было быстрее, чем в примерах сортировочной сети.

person Glenn Teitelbaum    schedule 24.05.2016

По причинам, аналогичным тем, которые я описал здесь, следующие функции сортировки sort6_iterator() и sort10_iterator_local() должны работать хорошо там, где сортировка сеть была взята из здесь:

template<class IterType> 
inline void sort10_iterator(IterType it) 
{
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   auto data##a=*(data+a);
#define DD2(a,b) auto data##a=*(data+a), data##b=*(data+b);
#define CB1(a)   *(data+a)=data##a;
#define CB2(a,b) *(data+a)=data##a;*(data+b)=data##b;
  DD2(1,4) SORT2(1,4) DD2(7,8) SORT2(7,8) DD2(2,3) SORT2(2,3) DD2(5,6) SORT2(5,6) DD2(0,9) SORT2(0,9) 
  SORT2(2,5) SORT2(0,7) SORT2(8,9) SORT2(3,6) 
  SORT2(4,9) SORT2(0,1) 
  SORT2(0,2) CB1(0) SORT2(6,9) CB1(9) SORT2(3,5) SORT2(4,7) SORT2(1,8) 
  SORT2(3,4) SORT2(5,8) SORT2(6,7) SORT2(1,2) 
  SORT2(7,8) CB1(8) SORT2(1,3) CB1(1) SORT2(2,5) SORT2(4,6) 
  SORT2(2,3) CB1(2) SORT2(6,7) CB1(7) SORT2(4,5) 
  SORT2(3,4) CB2(3,4) SORT2(5,6) CB2(5,6) 
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Чтобы вызвать эту функцию, я передал ей std::vector итератор.

person Matthew K.    schedule 07.06.2017

Сортировка вставкой требует в среднем 29,6 сравнений для сортировки 10 входных данных с лучшим случаем 9 и худшим 45 (учитывая входные данные в обратном порядке).

Сортировка оболочки {9,6,1} потребует в среднем 25,5 сравнений для сортировки 10 входных данных. В лучшем случае - 14 сравнений, в худшем - 34, а для сортировки обратного ввода требуется 22.

Таким образом, использование shellsort вместо сортировки вставкой снижает средний случай на 14%. Хотя в лучшем случае увеличивается на 56%, в худшем случае уменьшается на 24%, что важно для приложений, где важно контролировать производительность в худшем случае. Обратный случай уменьшается на 51%.

Поскольку вы, кажется, знакомы с сортировкой вставкой, вы можете реализовать алгоритм как сеть сортировки для {9,6}, а затем добавить сортировку вставкой ({1}) после этого:

i[0] with i[9]    // {9}

i[0] with i[6]    // {6}
i[1] with i[7]    // {6}
i[2] with i[8]    // {6}
i[3] with i[9]    // {6}

i[0 ... 9]        // insertion sort
person Olof Forshell    schedule 21.06.2016

Зачем менять местами, когда можно двигаться? В одной строке кэша x86 достаточно дополнительной памяти, чтобы вы могли выполнить сортировку слиянием.

Я бы, вероятно, вставил индексы сортировки 0–1, 2–4, 5–6, 7–9 по отдельности или, что еще лучше, сохранил бы эти маленькие группы отсортированными, как я делаю вставки, так что для каждой вставки требуется не более одной или двух смен.

Затем объедините 5,6 и 7-9 - ›10-14, объедините 0-1 и 2-4 -› 5-9 и, наконец, объедините 5-9 и 10-14 - ›0-9

person Matt Timmermans    schedule 11.07.2020