Отвечая на другой вопрос переполнения стека ( этот ), я наткнулся на интересную подзадачу. Какой самый быстрый способ сортировки массива из 6 целых чисел?
Как вопрос очень низкого уровня:
- мы не можем предполагать, что библиотеки доступны (и сам вызов имеет свою стоимость), только простой C
- чтобы избежать опустошения конвейера команд (который имеет очень высокую стоимость), мы, вероятно, должны минимизировать переходы, скачки и любые другие виды прерывания потока управления (например, те, которые скрыты за точками последовательности в
&&
или||
). - пространство ограничено, и минимизация регистров и использование памяти является проблемой, в идеале сортировка на месте, вероятно, лучше всего.
На самом деле этот вопрос - своего рода гольф, цель которого не в том, чтобы минимизировать длину источника, а во время выполнения. Я называю это «кодом Зенинга», который используется в названии книги « Дзен оптимизации кода». » Майкла Абраша и ее продолжений .
Что касается того, почему это интересно, есть несколько слоев:
- пример прост и легок для понимания и измерения, не требующий много навыков
- это показывает эффекты выбора хорошего алгоритма для проблемы, но также эффекты компилятора и основного оборудования.
Вот моя эталонная (наивная, не оптимизированная) реализация и мой набор тестов.
#include <stdio.h>
static __inline__ int sort6(int * d){
char j, i, imin;
int tmp;
for (j = 0 ; j < 5 ; j++){
imin = j;
for (i = j + 1; i < 6 ; i++){
if (d[i] < d[imin]){
imin = i;
}
}
tmp = d[j];
d[j] = d[imin];
d[imin] = tmp;
}
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int main(int argc, char ** argv){
int i;
int d[6][5] = {
{1, 2, 3, 4, 5, 6},
{6, 5, 4, 3, 2, 1},
{100, 2, 300, 4, 500, 6},
{100, 2, 3, 4, 500, 6},
{1, 200, 3, 4, 5, 600},
{1, 1, 2, 1, 2, 1}
};
unsigned long long cycles = rdtsc();
for (i = 0; i < 6 ; i++){
sort6(d[i]);
/*
* printf("d%d : %d %d %d %d %d %d\n", i,
* d[i][0], d[i][6], d[i][7],
* d[i][8], d[i][9], d[i][10]);
*/
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
}
Необработанные результаты
Поскольку количество вариантов становится большим, я собрал их все в наборе тестов, который можно найти здесь . Благодаря Кевину Сток, используемые тесты немного менее наивны, чем показанные выше. Вы можете скомпилировать и выполнить его в своей среде. Мне весьма интересно поведение на разных целевых архитектурах / компиляторах. (Хорошо, ребята, вставьте это в ответы, я буду +1 каждому вкладчику нового набора результатов).
Я дал ответ Даниэлю Штутцбаху (для игры в гольф) год назад, поскольку он был источником самого быстрого решения в то время (сортировка сетей).
Linux 64 бит, gcc 4.6.1 64 бит, Intel Core 2 Duo E8400, -O2
- Прямой вызов функции библиотеки qsort: 689,38
- Наивная реализация (вставка сортировки): 285.70
- Сортировка вставок (Даниэль Штутцбах): 142,12
- Развернутая сортировка вставок: 125.47
- Порядок ранга: 102.26
- Ранг Порядок с регистрами: 58.03
- Сортировка сетей (Даниэль Штутцбах): 111,68
- Сортировка сетей (Paul R): 66,36
- Сортировка сетей 12 с быстрой заменой: 58,86
- Сортировка сетей 12 переупорядоченный своп: 53,74
- Сортировка сетей 12 переупорядочен Простая замена: 31,54
- Переупорядоченная сеть сортировки с быстрой заменой: 31,54
- Переупорядоченная сеть сортировки с быстрой заменой V2: 33,63
- Сортированный пузырь (Паоло Бонзини): 48,85
- Развернутая сортировка вставок (Паоло Бонзини): 75,30
Linux 64 бит, gcc 4.6.1 64 бит, Intel Core 2 Duo E8400, -O1
- Прямой вызов функции библиотеки qsort: 705,93
- Наивная реализация (вставка сортировки): 135.60
- Сортировка вставок (Даниэль Штутцбах): 142,11
- Развернутая сортировка вставок: 126,75
- Порядок ранга: 46.42
- Порядок ранжирования с регистрами: 43,58
- Сортировка сетей (Даниэль Штутцбах): 115,57
- Сортировка сетей (Paul R): 64,44
- Сортировка сетей 12 с быстрой заменой: 61,98
- Сортировка сетей 12 переупорядочено Своп: 54.67
- Сортировка сетей 12 переупорядочен Простая замена: 31,54
- Переупорядоченная сеть сортировки с быстрой заменой: 31,24
- Переупорядоченная сеть сортировки с быстрой заменой V2: 33,07
- Сортированный пузырь (Паоло Бонзини): 45,79
- Развернутая сортировка вставок (Паоло Бонзини): 80,15
Я включил результаты как -O1, так и -O2, потому что неожиданно для нескольких программ O2 менее эффективен, чем O1. Интересно, какая специфическая оптимизация имеет этот эффект?
Комментарии к предлагаемым решениям
Сортировка вставок (Даниэль Штутцбах)
Как и ожидалось, минимизация веток действительно хорошая идея.
Сортировка сетей (Даниэль Штутцбах)
Лучше, чем сортировка вставок. Я задавался вопросом, не был ли главный эффект от избегания внешнего цикла. Я проверил развернутую сортировку вставок, чтобы проверить, и мы действительно получаем примерно одинаковые цифры (код здесь ).
Сортировка сетей (Пол Р)
Лучшее пока. Фактический код, который я использовал для тестирования, находится здесь . Пока не знаю, почему это почти в два раза быстрее, чем в другой сети сортировки. Передача параметров? Быстро макс?
Сортировка сетей 12 SWAP с быстрой заменой
По предложению Даниэля Штутцбаха, я объединил его сеть сортировки с 12 свопами и быстрый обмен без ответвлений (код здесь ). Это действительно быстрее, лучший пока с небольшим запасом (примерно 5%), как и следовало ожидать, используя 1 своп меньше.
Интересно также отметить, что обмен без ответвлений, по-видимому, намного (в 4 раза) менее эффективен, чем простой, используемый в архитектуре PPC.
Вызов библиотеки qsort
Чтобы дать еще одну контрольную точку, я также попытался, как предлагалось, просто вызвать библиотеку qsort (код здесь ). Как и ожидалось, он намного медленнее: в 10-30 раз медленнее ... как стало очевидно с новым набором тестов, основная проблема, похоже, заключается в начальной загрузке библиотеки после первого вызова, и она не так плохо сравнивается с другими версия. Это только в 3-20 раз медленнее в моем Linux. В некоторых архитектурах, используемых для тестирования другими, это кажется даже быстрее (я действительно удивлен тем, что библиотека qsort использует более сложный API).
Порядок ранга
Рекс Керр предложил другой совершенно другой метод: для каждого элемента массива вычисляют непосредственно его конечную позицию. Это эффективно, потому что порядок вычисления не требует ветвления. Недостаток этого метода состоит в том, что он занимает в три раза больше памяти массива (одна копия массива и переменные для хранения ранговых порядков). Результаты производительности очень удивительны (и интересны). На моей эталонной архитектуре с 32-битной ОС и Intel Core2 Quad E8300 число циклов было чуть ниже 1000 (как в случае сортировки сетей с разветвленной ветвью). Но когда он был скомпилирован и выполнен на моем 64-битном компьютере (Intel Core2 Duo), он работал намного лучше: он стал самым быстрым до сих пор. Я наконец узнал истинную причину. Моя 32-битная коробка использует gcc 4.4.1, а моя 64-битная коробка gcc 4.4.
обновление :
Как видно из опубликованных выше рисунков, этот эффект все еще усиливался в более поздних версиях gcc, и ранговый порядок стал в два раза быстрее, чем в любой другой альтернативе.
Сортировка сетей 12 с переупорядоченным свопом
Удивительная эффективность предложения Rex Kerr с gcc 4.4.3 заставила меня задуматься: как может программа, использующая в 3 раза больше памяти, работать быстрее, чем сети без ветвей сортировки? Моя гипотеза состояла в том, что у него было меньше зависимостей типа чтения после записи, что позволило лучше использовать планировщик суперскалярных команд x86. Это дало мне идею: изменить порядок перестановок, чтобы минимизировать зависимости чтения после записи. Проще говоря: когда вы делаете, SWAP(1, 2); SWAP(0, 2);
вы должны дождаться завершения первого обмена, прежде чем выполнять второй, потому что оба имеют доступ к общей ячейке памяти. При SWAP(1, 2); SWAP(4, 5);
этом процессор может выполнять оба параллельно. Я попробовал, и все работает как положено, сортировочные сети работают примерно на 10% быстрее.
Сортировка сетей 12 с помощью простого обмена
Через год после первоначального поста Стейнар Х. Гандерсон предложил не пытаться перехитрить компилятор и сохранить простой код подкачки. Это действительно хорошая идея, поскольку полученный код работает примерно на 40% быстрее! Он также предложил обмен, оптимизированный вручную с использованием встроенного кода сборки x86, который может сэкономить еще несколько циклов. Самое удивительное (в нем говорится о психологии программиста), что год назад никто из бывших в употреблении не пробовал эту версию свопинга. Код, который я использовал для тестирования, находится здесь . Другие предлагали другие способы написания быстрого свопинга на C, но он дает те же характеристики, что и простой с приличным компилятором.
«Лучший» код теперь выглядит следующим образом:
static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x)
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
const int b = max(d[x], d[y]); \
d[x] = a; d[y] = b; }
SWAP(1, 2);
SWAP(4, 5);
SWAP(0, 2);
SWAP(3, 5);
SWAP(0, 1);
SWAP(3, 4);
SWAP(1, 4);
SWAP(0, 3);
SWAP(2, 5);
SWAP(1, 3);
SWAP(2, 4);
SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}
Если мы считаем, что наш тестовый набор (и, да, он довольно плохой, просто преимущество в том, что он короткий, простой и легкий для понимания того, что мы измеряем), среднее число циклов полученного кода для одного вида будет меньше 40 циклов ( Выполнено 6 тестов). Это означает, что каждый обмен в среднем составляет 4 цикла. Я называю это удивительно быстро. Возможны ли другие улучшения?
x-y
иx+y
не вызовет переполнение или переполнение?__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
среде объясняется тем, что rdtsc помещает ответ в EDX: EAX, в то время как GCC ожидает его в одном 64-битном регистре. Вы можете увидеть ошибку, скомпилировав в -O3. Также см. Ниже мой комментарий Полу Р. о более быстром обмене.CMP EAX, EBX; SBB EAX, EAX
установит 0 или 0xFFFFFFFF вEAX
зависимости от тогоEAX
, больше или меньшеEBX
, соответственно.SBB
«вычесть с заимствованием», аналогADC
(«добавить с переносом»); бит состояния, на который вы ссылаетесь, является битом переноса. С другой стороны, я помню этоADC
иSBB
имел ужасную задержку и пропускную способность на Pentium 4 по сравнению сADD
иSUB
, и все еще был вдвое медленнее на основных процессорах. Начиная с 80386 есть также инструкцииSETcc
условного хранения иCMOVcc
условного перемещения, но они также медленные.Ответы:
Для любой оптимизации всегда лучше всего тестировать, тестировать, тестировать. Я бы попробовал хотя бы сортировку сетей и вставку сортировки. Если бы я делал ставки, я бы положил свои деньги на сортировку, основываясь на прошлом опыте.
Вы знаете что-нибудь о входных данных? Некоторые алгоритмы будут работать лучше с определенными видами данных. Например, сортировка вставкой работает лучше для отсортированных или почти отсортированных данных, поэтому она будет лучшим выбором, если вероятность почти отсортированных данных выше среднего.
Алгоритм, который вы опубликовали, похож на сортировку вставкой, но похоже, что вы свели к минимуму количество свопов за счет дополнительных сравнений. Сравнения намного дороже, чем свопы, потому что ветки могут привести к остановке конвейера команд.
Вот реализация сортировки вставкой:
Вот как я бы построил сортировочную сеть. Во-первых, используйте этот сайт для создания минимального набора макросов SWAP для сети соответствующей длины. Завершение этого в функции дает мне:
источник
n < SMALL_CONSTANT
.Вот реализация с использованием сортировки сетей :
Для этого вам действительно нужны очень эффективные функции без ветвей
min
иmax
реализаций, поскольку именно к этому и сводится этот код - последовательность операцийmin
иmax
операций (всего по 13 на каждый). Я оставляю это как упражнение для читателя.Обратите внимание, что эта реализация легко поддается векторизации (например, SIMD - в большинстве SIMD ISA есть инструкции min / max для векторов), а также к реализациям на GPU (например, CUDA - без разветвлений нет проблем с расхождением деформации и т. Д.).
Смотрите также: Быстрая реализация алгоритма для сортировки очень маленького списка.
источник
Sort3
будет быстрее (на большинстве архитектур, в любом случае), если вы заметили, что(a+b+c)-(min+max)
это центральное число.#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }
. Здесь я не использую?: For d [y], потому что он дает немного худшую производительность, но почти в шуме.Поскольку это целые числа, а сравнение быстрое, почему бы не вычислить порядок ранжирования каждого из них напрямую:
источник
0+1+2+3+4+5=15
Так как один из них отсутствует, 15 минус сумма остальных дает пропущенныйПохоже, я попал на вечеринку год спустя, но здесь мы идем ...
Глядя на сборку, сгенерированную gcc 4.5.2, я заметил, что загрузка и сохранение выполняются для каждого свопа, который действительно не нужен. Было бы лучше загрузить 6 значений в регистры, отсортировать их и сохранить обратно в память. Я приказал, чтобы грузы в магазинах были как можно ближе к тому месту, где регистры сначала нужны и в последний раз используются. Я также использовал SWAP-макрос Стейнара Г. Гандерсона. Обновление: я переключился на макрос SWAP Паоло Бонзини, который превращает gcc во что-то похожее на макрос Гандерсона, но gcc может лучше упорядочить инструкции, поскольку они не представлены как явная сборка.
Я использовал тот же порядок обмена, что и для сети с переупорядоченным обменом, которая указана как наиболее эффективная, хотя может быть и лучший порядок. Если я найду больше времени, я сгенерирую и протестирую несколько перестановок.
Я изменил код тестирования, чтобы рассмотреть более 4000 массивов и показать среднее число циклов, необходимых для сортировки каждого из них. На i5-650 я получаю ~ 34,1 циклов / сортировку (с использованием -O3), по сравнению с исходной переупорядоченной сеткой сортировки, получающей ~ 65,3 циклов / сортировку (с использованием -O1, бьет -O2 и -O3).
Я изменил модифицированный набор тестов, чтобы также сообщать о часах для каждого вида и запускать больше тестов (функция cmp была также обновлена для обработки переполнения целых чисел), вот результаты для некоторых различных архитектур. Я попытался провести тестирование на процессоре AMD, но rdtsc не надежен на имеющейся у меня X6 1100T.
источник
-O3
оптимизация не будет контрпродуктивной.#define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }
.Я наткнулся на этот вопрос от Google несколько дней назад, потому что мне также нужно было быстро отсортировать массив фиксированной длины из 6 целых чисел. В моем случае, однако, мои целые числа - только 8 бит (вместо 32), и у меня нет строгого требования использовать только C. Я думал, что в любом случае поделюсь своими результатами, если они кому-нибудь пригодятся ...
Я реализовал вариант сетевой сортировки в сборке, который использует SSE для максимально возможной векторизации операций сравнения и обмена. Требуется шесть «проходов», чтобы полностью отсортировать массив. Я использовал новый механизм для непосредственного преобразования результатов PCMPGTB (векторизованное сравнение) в случайные параметры для PSHUFB (векторизованный обмен), используя только PADDB (векторизованное добавление), а в некоторых случаях также инструкцию PAND (побитовое И).
Этот подход также имел побочный эффект от получения действительно выполнял функцию без ответвлений. Там нет никаких инструкций прыжка вообще.
Похоже, что эта реализация примерно на 38% быстрее, чем реализация, которая в настоящее время помечена как самая быстрая опция в вопросе («Сортировка сетей 12 с помощью простой замены»). Я изменил эту реализацию, чтобы использовать
char
элементы массива во время моего тестирования, чтобы сделать сравнение справедливым.Следует отметить, что такой подход может применяться к любому размеру массива до 16 элементов. Я ожидаю, что относительное преимущество в скорости перед альтернативами будет увеличиваться для больших массивов.
Код написан на MASM для процессоров x86_64 с SSSE3. Функция использует «новое» соглашение о вызовах Windows x64. Вот...
Вы можете скомпилировать это в исполняемый объект и связать его с вашим C-проектом. Для получения инструкций о том, как сделать это в Visual Studio, вы можете прочитать эту статью . Вы можете использовать следующий прототип C для вызова функции из вашего кода C:
источник
pxor / pinsrd xmm4, mem, 0
, чтобы просто использоватьmovd
!Тестовый код довольно плохой; он переполняет исходный массив (люди здесь не читают предупреждения компилятора?), printf печатает неправильные элементы, он использует .byte для rdtsc без веской причины, есть только один прогон (!), нет ничего проверяющего, что конечные результаты на самом деле правильные (поэтому очень легко «оптимизировать» что-то слегка неправильное), включенные тесты очень элементарны (без отрицательных чисел?), и ничто не мешает компилятору просто отбросить всю функцию как мертвый код.
Тем не менее, это также довольно легко улучшить в битонном сетевом решении; просто измените мин / макс / своп вещи на
и он у меня получается примерно на 65% быстрее (Debian gcc 4.4.5 с -O2, amd64, Core i7).
источник
Хотя мне очень нравится макрос подкачки при условии:
Я вижу улучшение (которое может сделать хороший компилятор):
Мы обращаем внимание на то, как работают min и max, и явно извлекаем общее подвыражение. Это полностью исключает минимальные и максимальные макросы.
источник
d[x]
вместоx
(то же самое дляy
), иd[y] < d[x]
для неравенства здесь (да, отличается от кода min / max).Никогда не оптимизируйте мин / макс без бенчмаркинга и просмотра фактической сборки, сгенерированной компилятором. Если я позволю GCC оптимизировать min с помощью инструкций условного перемещения, я получу ускорение на 33%:
(280 против 420 циклов в тестовом коде). Выполнение max с?: Более или менее то же самое, почти потеряно в шуме, но вышеупомянутое немного быстрее. Этот SWAP быстрее с GCC и Clang.
Компиляторы также выполняют исключительную работу по распределению регистров и анализу псевдонимов, эффективно перемещая d [x] в локальные переменные заранее и только копируя обратно в память в конце. На самом деле, они делают это даже лучше, чем если бы вы работали полностью с локальными переменными (например,
d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5]
). Я пишу это, потому что вы предполагаете сильную оптимизацию и все же пытаетесь перехитрить компилятор на мин / макс. :)Кстати, я пробовал Clang и GCC. Они выполняют одну и ту же оптимизацию, но из-за различий в расписании у обоих есть некоторые различия в результатах, не могу сказать, что на самом деле быстрее или медленнее. GCC быстрее в сетях сортировки, Clang в квадратичных сортировках.
Просто для полноты возможна развернутая сортировка пузырьков и вставка. Вот вид пузыря:
и вот сортировка вставки:
Такая сортировка вставок выполняется быстрее, чем у Даниеля Штутцбаха, и особенно хороша для графического процессора или компьютера с предопределением, потому что ITER может быть выполнен только с 3 инструкциями (против 4 для SWAP). Например, вот
t = d[2]; ITER(1); ITER(0);
строка в сборке ARM:Для шести элементов сортировка вставки является конкурентоспособной с сетью сортировки (12 обменов против 15 итераций балансирует 4 инструкции / обмен против 3 инструкций / итерация); пузырьковый тип, конечно, медленнее. Но это не будет правдой, когда размер увеличивается, так как сортировка вставки - O (n ^ 2), а сортировка сетей - O (n log n).
источник
Я перенес набор тестов на машину с архитектурой PPC, которую я не могу идентифицировать (не нужно было трогать код, просто увеличивать итерации теста, использовать 8 тестовых случаев, чтобы избежать загрязнения результатов модами и заменить специфичный для x86 rdtsc):
Прямой вызов функции библиотеки qsort : 101
Наивная реализация (вставка сортировки) : 299
Сортировка вставок (Даниэль Штутцбах) : 108
Развернутая сортировка вставок : 51
Сортировка сетей (Даниэль Штутцбах) : 26
Сортировка сетей (Paul R) : 85
Сортировка сетей 12 с быстрой заменой : 117
Сортировка сетей 12 переупорядочено Своп : 116
Порядок ранга : 56
источник
subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3
. r3 / r4 - входы, r5 / r6 - регистры нуля, на выходе r3 получает минимум, а r4 - максимум. Это должно быть прилично расписано вручную. Я нашел это с помощью супероптимизатора GNU, начиная с 4-минутных и максимальных последовательностей инструкций и просматривая вручную два, которые можно объединить. Для входов со знаком вы, конечно, можете добавить 0x80000000 ко всем элементам в начале и снова вычесть его в конце, а затем работать так, как если бы они были без знака.Обмен XOR может быть полезен в ваших функциях обмена.
If может вызвать слишком большое расхождение в вашем коде, но если у вас есть гарантия, что все ваши целые уникальны, это может быть удобно.
источник
x
иy
указывают на то же место.С нетерпением жду возможности попробовать свои силы и учиться на этих примерах, но сначала несколько моментов из моей 1,5 ГГц PPC Powerbook G4 с 1 ГБ оперативной памяти DDR. (Я позаимствовал подобный rdtsc-подобный таймер для PPC от http://www.mcs.anl.gov/~kazutomo/rdtsc.html для таймингов.) Я запускал программу несколько раз, и абсолютные результаты менялись, но последовательно Самым быстрым тестом был «Сортировка вставки (Даниэль Штутцбах)», а «Сортировка вставки развернута» за короткую секунду.
Вот последний набор раз:
источник
Вот мой вклад в эту тему: оптимизированная 1, 4-разрядная сортировка оболочки для вектора int из 6 элементов (valp), содержащего уникальные значения.
На моем ноутбуке HP dv7-3010so с двухъядерным процессором Athlon M300 @ 2 ГГц (память DDR2) он работает за 165 тактов. Это среднее значение, рассчитанное по времени для каждой уникальной последовательности (всего 6! / 720). Скомпилировано в Win32 с использованием OpenWatcom 1.8. Цикл по сути является сортировкой вставки и имеет длину 16 инструкций / 37 байт.
У меня нет 64-битной среды для компиляции.
источник
Если сортировка вставок здесь достаточно конкурентоспособна, я бы рекомендовал попробовать сортировку оболочек. Боюсь, что 6 элементов, вероятно, слишком мало для того, чтобы быть в числе лучших, но, возможно, стоит попробовать.
Пример кода, непроверенный, отлаженный и т. Д. Вы хотите настроить последовательность inc = 4 и inc - = 3, чтобы найти оптимальный (например, попробуйте inc = 2, inc - = 1).
Я не думаю, что это победит, но если кто-то отправит вопрос о сортировке 10 элементов, кто знает ...
Согласно Википедии это может даже сочетаться с сортировочными сетями: Pratt, V (1979). Сортировка и сортировка сетей (Выдающиеся диссертации в области компьютерных наук). Garland. ISBN 0-824-04406-1
источник
Я знаю, что я опоздал, но мне было интересно поэкспериментировать с разными решениями. Сначала я очистил эту пасту, скомпилировал ее и поместил в репозиторий. Я оставил некоторые нежелательные решения в тупик, чтобы другие не попробовали. Среди них было мое первое решение, которое пыталось гарантировать, что x1> x2 был вычислен один раз. После оптимизации он не быстрее других простых версий.
Я добавил зацикленную версию сортировки по рангу, поскольку мое собственное приложение для этого исследования предназначено для сортировки 2-8 элементов, поэтому, поскольку имеется переменное число аргументов, необходим цикл. По этой же причине я проигнорировал сетевые решения для сортировки.
Тестовый код не проверял правильность обработки дубликатов, поэтому, хотя все существующие решения были правильными, я добавил специальный код в тестовый код, чтобы гарантировать правильную обработку дубликатов.
Затем я написал вид вставки, который целиком находится в регистрах AVX. На моей машине это на 25% быстрее, чем другие типы вставок, но на 100% медленнее, чем порядок ранжирования. Я сделал это исключительно для эксперимента и не ожидал, что это будет лучше из-за разветвления в виде вставки.
Затем я написал сортировку по рангу, используя AVX. Это соответствует скорости других решений рангового порядка, но не быстрее. Проблема в том, что я могу рассчитывать индексы только с помощью AVX, а затем мне нужно составить таблицу индексов. Это связано с тем, что расчеты основаны на назначении, а не на источнике. См. Преобразование из исходных индексов в целевые индексы
Репо можно найти здесь: https://github.com/eyepatchParrot/sort6/
источник
vmovmskps
целочисленные векторы (с приведением, чтобы сохранить внутреннюю сущность счастливой), избегая необходимости сдвигать вправоffs
результат bitcan ( ).cmpgt
результата, вычтя его, вместо того, чтобы маскировать егоset1(1)
. например,index = _mm256_sub_epi32(index, gt)
делаетindex -= -1 or 0;
eq = _mm256_insert_epi32(eq, 0, I)
не эффективный способ обнуления элемента, если он компилируется как записано (особенно для элементов за пределами нижнего 4, потому чтоvpinsrd
доступно только с назначением XMM; индексы выше 3 должны эмулироваться). Вместо этого_mm256_blend_epi32
(vpblendd
) с нулевым вектором.vpblendd
это команда с одним мопом, которая выполняется на любом порту, в отличие от шаффла, которому нужен порт 5 на процессорах Intel. ( agner.org/optimize ).rot
векторов с разными перемешиваниями из одного и того же источника или, по крайней мере, запустить 2 цепочки депо параллельно, которые вы используете поочередно, вместо одной цепочки депе через перестановку, пересекающую полосу (задержка 3 цикла). Это увеличит ILP в пределах одного вида. 2 dep цепочки ограничивают число векторных констант разумным числом, просто 2: 1 для одного поворота и один для 2 шагов поворота вместе взятых.Этот вопрос становится довольно старым, но на самом деле мне пришлось решать ту же проблему в наши дни: быстрые агорифмы для сортировки небольших массивов. Я подумал, что будет хорошей идеей поделиться своими знаниями. Хотя я впервые начал использовать сортировку сетей, мне, наконец, удалось найти другие алгоритмы, для которых общее число сравнений, выполненных для сортировки каждой перестановки из 6 значений, было меньше, чем с сетями сортировки, и меньше, чем с сортировкой вставками. Я не посчитал количество свопов; Я ожидал бы, что это будет примерно эквивалентно (возможно, немного выше иногда).
Алгоритм
sort6
использует алгоритм,sort4
который использует алгоритмsort3
. Вот реализация в некоторой легкой форме C ++ (оригинал является тяжелым шаблоном, чтобы он мог работать с любым итератором с произвольным доступом и любой подходящей функцией сравнения).Сортировка 3 значений
Следующий алгоритм представляет собой развернутую сортировку вставок. Когда необходимо выполнить два обмена (6 назначений), вместо этого используются 4 назначения:
Это выглядит немного сложнее, потому что сортировка имеет более или менее одну ветвь для каждой возможной перестановки массива, используя 2 ~ 3 сравнения и не более 4 присваиваний для сортировки трех значений.
Сортировка 4 значений
Этот вызов вызывает
sort3
затем развернутую сортировку вставки с последним элементом массива:Этот алгоритм выполняет от 3 до 6 сравнений и не более 5 обменов. Развернуть сортировку вставкой легко, но мы будем использовать другой алгоритм для последней сортировки ...
Сортировка 6 значений
Этот использует развернутую версию того, что я назвал сортировкой с двойной вставкой . Название не настолько велико, но оно довольно наглядно, вот как оно работает:
После перестановки первый элемент всегда меньше последнего, что означает, что при вставке их в отсортированную последовательность будет не более N сравнений для вставки двух элементов в худшем случае: например, если первый элемент вставлен в 3-ю позицию, затем последний не может быть вставлен ниже 4-й позиции.
Мои тесты на каждую перестановку из 6 значений показывают, что этот алгоритм всегда выполняет от 6 до 13 сравнений. Я не вычислял количество выполненных свопов, но не ожидаю, что оно будет выше 11 в худшем случае.
Я надеюсь, что это поможет, даже если этот вопрос больше не представляет реальной проблемы :)
РЕДАКТИРОВАТЬ: после того, как положить его в предоставленный тест, он будет медленнее, чем большинство интересных альтернатив. Это имеет тенденцию работать немного лучше, чем развернутая сортировка вставок, но это в значительной степени так. По сути, это не лучший тип для целых чисел, но он может быть интересен для типов с дорогой операцией сравнения.
источник
operator<
для сравнения. Помимо объективного количества сравнений и свопов, я также правильно рассчитал свои алгоритмы; это решение было самым быстрым, но я действительно пропустил @ RexKerr. Собираюсь попробовать :)-O3
. Я предполагаю, что тогда я приму другую стратегию для своей библиотеки сортировки: предоставляя три вида алгоритмов, которые будут иметь либо небольшое количество сравнений, либо небольшое количество перестановок, либо потенциально лучшую производительность. По крайней мере, то, что произойдет, будет прозрачным для читателя. Спасибо за ваши идеи :)Я считаю, что на ваш вопрос есть две части.
Я бы не стал сильно волноваться по поводу опустошения конвейеров (если принять текущий x86): предсказание ветвлений прошло долгий путь. То, о чем я бы беспокоился, - это убедиться, что код и данные помещаются в одну строку кэша каждая (возможно, две для кода). Как только время выборки будет очень низким, это компенсирует любую задержку. Это также означает, что ваш внутренний цикл будет состоять из десяти инструкций или около того, что и должно быть (в моем алгоритме сортировки есть два разных внутренних цикла, они 10 инструкций / 22 байта и 9/22 длиной соответственно). Предполагая, что код не содержит никаких div-ов, вы можете быть уверены, что он будет ослепительно быстрым.
источник
Я знаю, что это старый вопрос.
Но я просто написал другое решение, которым хочу поделиться.
Не используя ничего, кроме вложенного MIN MAX,
Это не быстро, так как использует 114 из каждого,
может уменьшить его до 75, просто так -> pastebin
Но тогда это уже не просто min max.
Что может работать, так это делать мин / макс для нескольких целых чисел одновременно с AVX
Ссылка PMINSW
РЕДАКТИРОВАТЬ:
решение порядка ранга вдохновлен Рекс Керр, гораздо быстрее, чем беспорядок выше
источник
int16_t
). Но ваша функция C утверждает, что она сортирует массивint
(который является 32-битным во всех реализациях C, которые поддерживают этотasm
синтаксис). Вы проверяли это только с маленькими положительными целыми числами, у которых только 0 в их старших половинах? Это сработает ... Дляint
вас нужен SSE4.1pmin/maxsd
(d = dword). felixcloutier.com/x86/pminsd:pminsq илиpminusd
дляuint32_t
.Я обнаружил , что по крайней мере на моей системе, функции
sort6_iterator()
иsort6_iterator_local()
определены ниже как бегала по крайней мере так же быстро, и часто заметно быстрее, чем выше текущего рекордсмена:Я передал эту функцию в качестве
std::vector
итератора в моем временном коде.Я подозреваю (из комментариев, подобных этому и в других местах), что использование итераторов дает g ++ определенные гарантии того, что может и не может произойти с памятью, на которую ссылается итератор, чего в противном случае не было бы, и именно эти гарантии позволяют g ++ лучше оптимизировать код сортировки (например, с помощью указателей компилятор не может быть уверен, что все указатели указывают на разные области памяти). Если я правильно помню, это тоже часть из причин , почему так много алгоритмов STL, такие как
std::sort()
, как правило , имеют такую нецензурно производительность хорошая.Более того,
sort6_iterator()
в некоторых случаях (опять же, в зависимости от контекста, в котором вызывается функция) последовательно выполняется следующая функция сортировки, которая копирует данные в локальные переменные перед их сортировкой. 1 Обратите внимание, что, поскольку определены только 6 локальных переменных, если эти локальные переменные являются примитивами, то они, скорее всего, фактически никогда не хранятся в ОЗУ, а вместо этого хранятся только в регистрах ЦП до конца вызова функции, что помогает производить такую сортировку. функционировать быстро (Также помогает компилятору знать, что разные локальные переменные имеют разные места в памяти).Обратите внимание, что определение, приведенное
SWAP()
ниже, в некоторых случаях приводит к немного лучшей производительности, хотя в большинстве случаев это приводит к несколько худшей производительности или незначительной разнице в производительности.Если вы просто хотите алгоритм сортировки, который в примитивных типах данных, gcc -O3 неизменно хорош в оптимизации, независимо от того, в каком контексте вызов функции сортировки появляется в 1, то, в зависимости от того, как вы передаете ввод, попробуйте один из следующих двух алгоритмы:
Или, если вы хотите передать переменные по ссылке, используйте это (функция ниже отличается от вышеупомянутой в ее первых 5 строках):
Причина использования
register
ключевого слова в том, что это один из тех немногих случаев, когда вы знаете, что вам нужны эти значения в регистрах. Безregister
этого компилятор будет понимать это большую часть времени, но иногда это не так. Использованиеregister
ключевого слова помогает решить эту проблему. Обычно, однако, не используйтеregister
ключевое слово, так как оно скорее замедлит ваш код, чем ускорит его.Также обратите внимание на использование шаблонов. Это сделано специально, поскольку даже при использовании
inline
ключевого слова функции шаблонов обычно гораздо более агрессивно оптимизируются с помощью gcc, чем функции vanilla C (это связано с тем, что gcc необходимо иметь дело с указателями функций для функций vanilla C, но не с функциями шаблонов).источник
Попробуйте "объединить отсортированный список" сортировать. :) Используйте два массива. Самый быстрый для малого и большого массива.
Если вы согласны, вы только проверяете, где вставить. Другие большие значения вам не нужно сравнивать (cmp = ab> 0).
Для 4 номеров вы можете использовать систему 4-5 cmp (~ 4.6) или 3-6 cmp (~ 4.9). Bubble sort использовать 6 cmp (6). Много cmp для больших чисел медленнее кода.
Этот код использует 5 cmp (не сортировка MSL):
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}
Принципиальный MSL
9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8
JS код
источник
Сортировать 4 элемента с использованием cmp == 0. Числа cmp ~ 4.34 (у родной FF ~ 4.52), но занимают в 3 раза больше времени, чем слияние списков. Но лучше меньше операций cmp, если у вас большие цифры или большой текст. Редактировать: исправлена ошибка
Онлайн тест http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm
источник
Может быть , я буду поздно к партии, но , по крайней мере , мой вклад является новым подходом.
swap
будет выше (irt стоимостьcompare
)SWAP()
двух элементов циклы преследуются, для чего требуется только одна временная замена и один (регистр-> регистр) своп (новый <- старый).Обновление: немного изменил код, некоторые люди используют компиляторы C ++ для компиляции кода C ...
источник
wsort6()
функция имеет правильный интерфейс.o1..o5
, нет необходимости во втором временномe[6]
массиве. И: компилирование кода C на компиляторе C ++ и обвинение кода?#include
. Исправленоисточник
Хорошо, если это всего 6 элементов, и вы можете использовать параллелизм, хотите минимизировать условное ветвление и т. Д. Почему вы не генерируете все комбинации и не проверяете порядок? Рискну предположить, что в некоторых архитектурах это может быть довольно быстро (при условии, что у вас предварительно выделена память)
источник
Вот три типичных метода сортировки, которые представляют три разных класса алгоритмов сортировки:
Но посмотрите обсуждение Стефаном Нельссоном самого быстрого алгоритма сортировки? где он обсуждает решение, которое сводится к
O(n log log n)
.. проверить его реализацию в CЭтот алгоритм полулинейной сортировки был представлен в статье в 1995 году:
А. Андерссон, Т. Хагеруп, С. Нильссон и Р. Раман. Сортировка по линейному времени? В материалах 27-го ежегодного симпозиума ACM по теории вычислений, стр. 427-436, 1995.
источник