Отвечая на другой вопрос (этот), я наткнулся на интересную подзадачу. Каков самый быстрый способ сортировки массива из 6 ints?
Как вопрос очень низкий уровень:
&&
или ||
).Действительно, этот вопрос - это своего рода гольф, целью которого является не минимизация длины источника, а времени выполнения. Я называю это кодом "Zening", как используется в названии книги Zen of Code optimization Michael Abrash и его сиквелы.
Что касается того, почему это интересно, существует несколько слоев:
Вот моя ссылка (наивная, не оптимизированная) реализация и мой набор тестов.
#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
Linux 64 бит, gcc 4.6.1 64 бит, Intel Core 2 Duo E8400, -O1
Я включил результаты как -O1 и -O2, потому что неожиданно для нескольких программ O2 меньше эффективнее, чем O1. Интересно, какая именно оптимизация имеет этот эффект?
Вставка Сортировка (Daniel Stutzbach)
Как и ожидалось, минимизация ветвей действительно хорошая идея.
Сортировка сетей (Daniel Stutzbach)
Лучше, чем сортировка вставки. Я задавался вопросом, не вытекает ли главный эффект из-за отсутствия внешнего контура. Я попробовал разворачивать сортировку вставки, чтобы проверить, и действительно мы получаем примерно одинаковые цифры (код здесь).
Сортировка сетей (Paul R)
Лучшее до сих пор. Фактический код, который я использовал для тестирования, здесь. Пока не знаю, почему это почти в два раза быстрее, чем реализация другой сортировочной сети. Передача параметров? Быстрый макс?
Сортировка сетей 12 SWAP с быстрой заменой
Как предложил Даниэль Штуцбах, я объединил свою 12 сортировочную сеть для свопинга с нераспределенной быстрой заменой (код здесь). Это действительно быстрее, лучше всего с небольшим отрывом (примерно 5%), как и следовало ожидать, используя 1 меньший своп.
Также интересно заметить, что разветвленный своп, по-видимому, намного (в 4 раза) менее эффективен, чем простой, используя if на архитектуре 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.3, и последний выглядит намного лучше для оптимизации этого кода (для других предложений было очень мало).
обновление:
Как опубликованные выше цифры показывают, что этот эффект все же был усилен более поздними версиями gcc и Rank Order, которые стали последовательно в два раза быстрее, чем любая другая альтернатива.
Сортировка сетей 12 с переупорядоченным свопом
Удивительная эффективность предложения Rex Kerr с gcc 4.4.3 заставило меня задуматься: как могла бы программа с 3-кратным потреблением памяти быстрее, чем бесконтактные сортировочные сети? Моя гипотеза заключалась в том, что у нее было меньше зависимостей вида, читаемого после записи, что позволяет лучше использовать суперскалярный планировщик команд x86. Это дало мне представление: переупорядочить свопы, чтобы свести к минимуму количество записей после зависимостей записи. Проще говоря: когда вы делаете SWAP(1, 2); SWAP(0, 2);
, вам нужно дождаться завершения первого свопа перед выполнением второго, потому что оба доступа к общей ячейке памяти. Когда вы выполняете SWAP(1, 2); SWAP(4, 5);
, процессор может выполнять оба параллельно. Я попробовал, и он работает так, как ожидалось, сортировочные сети работают на 10% быстрее.
Сортировка сетей 12 с простой заменой
Через год после первоначального сообщения Steinar H. Gunderson предположил, что мы не должны пытаться перехитрить компилятор и держать код свопа простым. Это действительно хорошая идея, поскольку полученный код примерно на 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 цикла. Я называю это удивительно быстро. Возможны ли другие улучшения?
Для любой оптимизации всегда лучше тестировать, тестировать, тестировать. Я бы попробовал, по крайней мере, сортировку сетей и сортировку вставки. Если бы я держал пари, я бы поместил свои деньги на сортировку вставки, основываясь на прошлом опыте.
Знаете ли вы что-нибудь о входных данных? Некоторые алгоритмы будут работать лучше с некоторыми видами данных. Например, сортировка вставки лучше выполняется для отсортированных или почти отсортированных данных, поэтому это будет лучший выбор, если есть вероятность превышения по сравнению со средним значением почти отсортированных данных.
Опубликованный вами алгоритм похож на сортировку вставки, но похоже, что вы минимизировали количество свопов за счет большего количества сравнений. Однако сравнение гораздо дороже, чем свопы, поскольку ветки могут привести к остановке конвейера команд.
Здесь выполняется реализация сортировки вставки:
static __inline__ int sort6(int *d){
int i, j;
for (i = 1; i < 6; i++) {
int tmp = d[i];
for (j = i; j >= 1 && tmp < d[j-1]; j--)
d[j] = d[j-1];
d[j] = tmp;
}
}
Вот как я построил бы сортирующую сеть. Сначала используйте этот сайт для создания минимального набора макросов SWAP для сети соответствующей длины. Обертывание этого в функции дает мне:
static __inline__ int sort6(int * d){
#define SWAP(x,y) if (d[y] < d[x]) { int tmp = d[x]; d[x] = d[y]; d[y] = tmp; }
SWAP(1, 2);
SWAP(0, 2);
SWAP(0, 1);
SWAP(4, 5);
SWAP(3, 5);
SWAP(3, 4);
SWAP(0, 3);
SWAP(1, 4);
SWAP(2, 5);
SWAP(2, 4);
SWAP(1, 3);
SWAP(2, 3);
#undef SWAP
}
Здесь реализация с использованием сортировочных сетей:
inline void Sort2(int *p0, int *p1)
{
const int temp = min(*p0, *p1);
*p1 = max(*p0, *p1);
*p0 = temp;
}
inline void Sort3(int *p0, int *p1, int *p2)
{
Sort2(p0, p1);
Sort2(p1, p2);
Sort2(p0, p1);
}
inline void Sort4(int *p0, int *p1, int *p2, int *p3)
{
Sort2(p0, p1);
Sort2(p2, p3);
Sort2(p0, p2);
Sort2(p1, p3);
Sort2(p1, p2);
}
inline void Sort6(int *p0, int *p1, int *p2, int *p3, int *p4, int *p5)
{
Sort3(p0, p1, p2);
Sort3(p3, p4, p5);
Sort2(p0, p3);
Sort2(p2, p5);
Sort4(p1, p2, p3, p4);
}
Для этого вам понадобятся очень эффективные для этого разветвленные min
и max
реализации, так как это фактически то, к чему этот код сводится - последовательность операций min
и max
(всего 13 из них). Я оставляю это как упражнение для читателя.
Обратите внимание, что эта реализация легко поддается векторизации (например, SIMD - большинство SIMD ISAs имеют векторные инструкции min/max), а также к реализациям графического процессора (например, CUDA - бездисковое отсутствие проблем с деформированием warp и т.д.).
См. Также: Быстрая реализация алгоритма для сортировки очень малого списка
Так как это целые числа, а сравнения - быстрые, почему бы не вычислить порядок рангов каждого из них:
inline void sort6(int *d) {
int e[6];
memcpy(e,d,6*sizeof(int));
int o0 = (d[0]>d[1])+(d[0]>d[2])+(d[0]>d[3])+(d[0]>d[4])+(d[0]>d[5]);
int o1 = (d[1]>=d[0])+(d[1]>d[2])+(d[1]>d[3])+(d[1]>d[4])+(d[1]>d[5]);
int o2 = (d[2]>=d[0])+(d[2]>=d[1])+(d[2]>d[3])+(d[2]>d[4])+(d[2]>d[5]);
int o3 = (d[3]>=d[0])+(d[3]>=d[1])+(d[3]>=d[2])+(d[3]>d[4])+(d[3]>d[5]);
int o4 = (d[4]>=d[0])+(d[4]>=d[1])+(d[4]>=d[2])+(d[4]>=d[3])+(d[4]>d[5]);
int o5 = 15-(o0+o1+o2+o3+o4);
d[o0]=e[0]; d[o1]=e[1]; d[o2]=e[2]; d[o3]=e[3]; d[o4]=e[4]; d[o5]=e[5];
}
Похоже, я попал на вечеринку в конце года, но здесь мы идем...
Глядя на сборку, сгенерированную gcc 4.5.2, я заметил, что нагрузки и магазины выполняются для каждого свопа, что действительно не нужно. Было бы лучше загрузить 6 значений в регистры, отсортировать их и сохранить их обратно в память. Я приказал, чтобы нагрузки в магазинах были как можно ближе к тому, чтобы регистры были сначала необходимы и использовались в последний раз. Я также использовал SWAP-макрос Steinar H. Gunderson. Обновление: я переключился на макрос SWAP Paolo Bonzini, который gcc конвертирует в нечто похожее на Gunderson, но gcc может лучше упорядочить инструкции, поскольку они не указаны как явная сборка.
Я использовал тот же порядок подкачки, что и переупорядоченная сеть подкачки, которую вы выбрали как наиболее эффективный, хотя может быть и лучший порядок. Если я найду еще какое-то время, я создам и проведу кучу перестановок.
Я изменил код тестирования, чтобы рассмотреть более 4000 массивов и показать среднее количество циклов, необходимых для сортировки каждого из них. На i5-650 я получаю ~ 34.1 циклов/сортировку (используя -O3) по сравнению с исходной переупорядоченной сетью сортировки, получающей ~ 65.3 циклов/сортировку (с использованием -O1, бит -O2 и -O3).
#include <stdio.h>
static inline void sort6_fast(int * d) {
#define SWAP(x,y) { int dx = x, dy = y, tmp; tmp = x = dx < dy ? dx : dy; y ^= dx ^ tmp; }
register int x0,x1,x2,x3,x4,x5;
x1 = d[1];
x2 = d[2];
SWAP(x1, x2);
x4 = d[4];
x5 = d[5];
SWAP(x4, x5);
x0 = d[0];
SWAP(x0, x2);
x3 = d[3];
SWAP(x3, x5);
SWAP(x0, x1);
SWAP(x3, x4);
SWAP(x1, x4);
SWAP(x0, x3);
d[0] = x0;
SWAP(x2, x5);
d[5] = x5;
SWAP(x1, x3);
d[1] = x1;
SWAP(x2, x4);
d[4] = x4;
SWAP(x2, x3);
d[2] = x2;
d[3] = x3;
#undef SWAP
#undef min
#undef max
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile ("rdtsc; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
return x;
}
void ran_fill(int n, int *a) {
static int seed = 76521;
while (n--) *a++ = (seed = seed *1812433253 + 12345);
}
#define NTESTS 4096
int main() {
int i;
int d[6*NTESTS];
ran_fill(6*NTESTS, d);
unsigned long long cycles = rdtsc();
for (i = 0; i < 6*NTESTS ; i+=6) {
sort6_fast(d+i);
}
cycles = rdtsc() - cycles;
printf("Time is %.2lf\n", (double)cycles/(double)NTESTS);
for (i = 0; i < 6*NTESTS ; i+=6) {
if (d[i+0] > d[i+1] || d[i+1] > d[i+2] || d[i+2] > d[i+3] || d[i+3] > d[i+4] || d[i+4] > d[i+5])
printf("d%d : %d %d %d %d %d %d\n", i,
d[i+0], d[i+1], d[i+2],
d[i+3], d[i+4], d[i+5]);
}
return 0;
}
Я изменил измененный набор тестов, чтобы также сообщать о часах в сортировке и запускать больше тестов (функция cmp была обновлена для обработки переполнения целых чисел), вот результаты по некоторым различным архитектурам. Я попытался протестировать процессор AMD, но rdtsc не является надежным на X6 1100T, который у меня есть.
Clarkdale (i5-650)
==================
Direct call to qsort library function 635.14 575.65 581.61 577.76 521.12
Naive implementation (insertion sort) 538.30 135.36 134.89 240.62 101.23
Insertion Sort (Daniel Stutzbach) 424.48 159.85 160.76 152.01 151.92
Insertion Sort Unrolled 339.16 125.16 125.81 129.93 123.16
Rank Order 184.34 106.58 54.74 93.24 94.09
Rank Order with registers 127.45 104.65 53.79 98.05 97.95
Sorting Networks (Daniel Stutzbach) 269.77 130.56 128.15 126.70 127.30
Sorting Networks (Paul R) 551.64 103.20 64.57 73.68 73.51
Sorting Networks 12 with Fast Swap 321.74 61.61 63.90 67.92 67.76
Sorting Networks 12 reordered Swap 318.75 60.69 65.90 70.25 70.06
Reordered Sorting Network w/ fast swap 145.91 34.17 32.66 32.22 32.18
Kentsfield (Core 2 Quad)
========================
Direct call to qsort library function 870.01 736.39 723.39 725.48 721.85
Naive implementation (insertion sort) 503.67 174.09 182.13 284.41 191.10
Insertion Sort (Daniel Stutzbach) 345.32 152.84 157.67 151.23 150.96
Insertion Sort Unrolled 316.20 133.03 129.86 118.96 105.06
Rank Order 164.37 138.32 46.29 99.87 99.81
Rank Order with registers 115.44 116.02 44.04 116.04 116.03
Sorting Networks (Daniel Stutzbach) 230.35 114.31 119.15 110.51 111.45
Sorting Networks (Paul R) 498.94 77.24 63.98 62.17 65.67
Sorting Networks 12 with Fast Swap 315.98 59.41 58.36 60.29 55.15
Sorting Networks 12 reordered Swap 307.67 55.78 51.48 51.67 50.74
Reordered Sorting Network w/ fast swap 149.68 31.46 30.91 31.54 31.58
Sandy Bridge (i7-2600k)
=======================
Direct call to qsort library function 559.97 451.88 464.84 491.35 458.11
Naive implementation (insertion sort) 341.15 160.26 160.45 154.40 106.54
Insertion Sort (Daniel Stutzbach) 284.17 136.74 132.69 123.85 121.77
Insertion Sort Unrolled 239.40 110.49 114.81 110.79 117.30
Rank Order 114.24 76.42 45.31 36.96 36.73
Rank Order with registers 105.09 32.31 48.54 32.51 33.29
Sorting Networks (Daniel Stutzbach) 210.56 115.68 116.69 107.05 124.08
Sorting Networks (Paul R) 364.03 66.02 61.64 45.70 44.19
Sorting Networks 12 with Fast Swap 246.97 41.36 59.03 41.66 38.98
Sorting Networks 12 reordered Swap 235.39 38.84 47.36 38.61 37.29
Reordered Sorting Network w/ fast swap 115.58 27.23 27.75 27.25 26.54
Nehalem (Xeon E5640)
====================
Direct call to qsort library function 911.62 890.88 681.80 876.03 872.89
Naive implementation (insertion sort) 457.69 236.87 127.68 388.74 175.28
Insertion Sort (Daniel Stutzbach) 317.89 279.74 147.78 247.97 245.09
Insertion Sort Unrolled 259.63 220.60 116.55 221.66 212.93
Rank Order 140.62 197.04 52.10 163.66 153.63
Rank Order with registers 84.83 96.78 50.93 109.96 54.73
Sorting Networks (Daniel Stutzbach) 214.59 220.94 118.68 120.60 116.09
Sorting Networks (Paul R) 459.17 163.76 56.40 61.83 58.69
Sorting Networks 12 with Fast Swap 284.58 95.01 50.66 53.19 55.47
Sorting Networks 12 reordered Swap 281.20 96.72 44.15 56.38 54.57
Reordered Sorting Network w/ fast swap 128.34 50.87 26.87 27.91 28.02
-O3
не будет контрпродуктивной.
Я наткнулся на этот вопрос из Google несколько дней назад, потому что мне также пришлось быстро отсортировать массив фиксированной длины из 6 целых чисел. В моем случае, однако, мои целые числа составляют всего 8 бит (вместо 32), и у меня нет строгого требования использовать только C. Я думал, что буду делиться своими результатами так или иначе, в случае, если они могут быть полезны кому-то...
Я реализовал вариант сортировки сети в сборке, который использует SSE для векторизации операций сравнения и свопинга, насколько это возможно. Для полного сортировки массива требуется шесть "проходов". Я использовал новый механизм для прямого преобразования результатов PCMPGTB (векторизованного сравнения) для перетасовки параметров для PSHUFB (векторизованный своп), используя только PADDB (векторизованное дополнение), а в некоторых случаях также команду PAND (побитовое И).
Этот подход также имел побочный эффект, дающий действительно бесветренную функцию. Инструкции о прыжках отсутствуют.
Похоже, что эта реализация примерно на 38% быстрее, чем реализация, которая в настоящее время отмечена как самый быстрый вариант в вопросе ( "Сортировка сетей 12 с простой заменой" ). Я модифицировал эту реализацию для использования элементов массива char
во время моего тестирования, чтобы сделать сравнение справедливым.
Следует отметить, что этот подход может применяться к любому размеру массива до 16 элементов. Я ожидаю, что преимущество относительной скорости над альтернативами будет увеличиваться для больших массивов.
Код написан в MASM для процессоров x86_64 с SSSE3. Функция использует "новое" соглашение на использование Windows x64. Вот оно...
PUBLIC simd_sort_6
.DATA
ALIGN 16
pass1_shuffle OWORD 0F0E0D0C0B0A09080706040503010200h
pass1_add OWORD 0F0E0D0C0B0A09080706050503020200h
pass2_shuffle OWORD 0F0E0D0C0B0A09080706030405000102h
pass2_and OWORD 00000000000000000000FE00FEFE00FEh
pass2_add OWORD 0F0E0D0C0B0A09080706050405020102h
pass3_shuffle OWORD 0F0E0D0C0B0A09080706020304050001h
pass3_and OWORD 00000000000000000000FDFFFFFDFFFFh
pass3_add OWORD 0F0E0D0C0B0A09080706050404050101h
pass4_shuffle OWORD 0F0E0D0C0B0A09080706050100020403h
pass4_and OWORD 0000000000000000000000FDFD00FDFDh
pass4_add OWORD 0F0E0D0C0B0A09080706050403020403h
pass5_shuffle OWORD 0F0E0D0C0B0A09080706050201040300h
pass5_and OWORD 0000000000000000000000FEFEFEFE00h
pass5_add OWORD 0F0E0D0C0B0A09080706050403040300h
pass6_shuffle OWORD 0F0E0D0C0B0A09080706050402030100h
pass6_add OWORD 0F0E0D0C0B0A09080706050403030100h
.CODE
simd_sort_6 PROC FRAME
.endprolog
; pxor xmm4, xmm4
; pinsrd xmm4, dword ptr [rcx], 0
; pinsrb xmm4, byte ptr [rcx + 4], 4
; pinsrb xmm4, byte ptr [rcx + 5], 5
; The benchmarked 38% faster mentioned in the text was with the above slower sequence that tied up the shuffle port longer. Same on extract
; avoiding pins/extrb also means we don't need SSE 4.1, but SSSE3 CPUs without SSE4.1 (e.g. Conroe/Merom) have slow pshufb.
movd xmm4, dword ptr [rcx]
pinsrw xmm4, word ptr [rcx + 4], 2 ; word 2 = bytes 4 and 5
movdqa xmm5, xmm4
pshufb xmm5, oword ptr [pass1_shuffle]
pcmpgtb xmm5, xmm4
paddb xmm5, oword ptr [pass1_add]
pshufb xmm4, xmm5
movdqa xmm5, xmm4
pshufb xmm5, oword ptr [pass2_shuffle]
pcmpgtb xmm5, xmm4
pand xmm5, oword ptr [pass2_and]
paddb xmm5, oword ptr [pass2_add]
pshufb xmm4, xmm5
movdqa xmm5, xmm4
pshufb xmm5, oword ptr [pass3_shuffle]
pcmpgtb xmm5, xmm4
pand xmm5, oword ptr [pass3_and]
paddb xmm5, oword ptr [pass3_add]
pshufb xmm4, xmm5
movdqa xmm5, xmm4
pshufb xmm5, oword ptr [pass4_shuffle]
pcmpgtb xmm5, xmm4
pand xmm5, oword ptr [pass4_and]
paddb xmm5, oword ptr [pass4_add]
pshufb xmm4, xmm5
movdqa xmm5, xmm4
pshufb xmm5, oword ptr [pass5_shuffle]
pcmpgtb xmm5, xmm4
pand xmm5, oword ptr [pass5_and]
paddb xmm5, oword ptr [pass5_add]
pshufb xmm4, xmm5
movdqa xmm5, xmm4
pshufb xmm5, oword ptr [pass6_shuffle]
pcmpgtb xmm5, xmm4
paddb xmm5, oword ptr [pass6_add]
pshufb xmm4, xmm5
;pextrd dword ptr [rcx], xmm4, 0 ; benchmarked with this
;pextrb byte ptr [rcx + 4], xmm4, 4 ; slower version
;pextrb byte ptr [rcx + 5], xmm4, 5
movd dword ptr [rcx], xmm4
pextrw word ptr [rcx + 4], xmm4, 2 ; x86 is little-endian, so this is the right order
ret
simd_sort_6 ENDP
END
Вы можете скомпилировать это для исполняемого объекта и связать его с вашим проектом C. Инструкции о том, как это сделать в Visual Studio, можно прочитать в этой статье. Вы можете использовать следующий прототип C для вызова функции из вашего кода C:
void simd_sort_6(char *values);
Тест-код довольно плохой; он переполняет исходный массив (не читают ли люди здесь предупреждения компилятора?), printf печатает неправильные элементы, он использует .byte для rdtsc без уважительной причины, там только один запуск (!), там ничего не проверяют, что конечные результаты на самом деле правильные (поэтому очень легко "оптимизировать" что-то неточно неправильно), включенные тесты очень рудиментарны (нет отрицательных чисел?), и нет ничего, что могло бы заставить компилятор просто отбросить всю функцию как мертвый код.
Это, как говорится, также довольно легко улучшает решение битовой сети; просто измените материал min/max/SWAP на
#define SWAP(x,y) { int tmp; asm("mov %0, %2 ; cmp %1, %0 ; cmovg %1, %0 ; cmovg %2, %1" : "=r" (d[x]), "=r" (d[y]), "=r" (tmp) : "0" (d[x]), "1" (d[y]) : "cc"); }
и для меня это получается примерно на 65% (Debian gcc 4.4.5 с -O2, amd64, Core i7).
Пока мне действительно нравится макрос подкачки:
#define min(x, y) (y ^ ((x ^ y) & -(x < y)))
#define max(x, y) (x ^ ((x ^ y) & -(x < y)))
#define SWAP(x,y) { int tmp = min(d[x], d[y]); d[y] = max(d[x], d[y]); d[x] = tmp; }
Я вижу улучшение (которое может сделать хороший компилятор):
#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }
Мы принимаем во внимание, как работают min и max и вытягивают общее подвыражение явно. Это полностью исключает макросы min и max.
d[x]
вместо x
(то же самое для y
) и d[y] < d[x]
для неравенства здесь (да, отличается от кода min / max).
Никогда не оптимизируйте min/max без бенчмаркинга и посмотрите на сборку, созданную компилятором. Если я позволю GCC оптимизировать min с условными инструкциями перемещения, я получаю ускорение на 33%:
#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }
(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]
). Я пишу это, потому что вы принимаете сильную оптимизацию и все же пытаетесь перехитрить компилятор на min/max.:)
Кстати, я пробовал Clang и GCC. Они выполняют ту же оптимизацию, но из-за различий в расписании два имеют некоторые вариации в результатах, не могут сказать действительно, что происходит быстрее или медленнее. GCC работает быстрее в сортировочных сетях, Clang на квадратичных сортировках.
Просто для полноты, возможны разбросанные сортировки и сортировки в виде пузырей. Вот сортировка пузырьков:
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4); SWAP(4,5);
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4);
SWAP(0,1); SWAP(1,2); SWAP(2,3);
SWAP(0,1); SWAP(1,2);
SWAP(0,1);
и вот сортировка вставки:
//#define ITER(x) { if (t < d[x]) { d[x+1] = d[x]; d[x] = t; } }
//Faster on x86, probably slower on ARM or similar:
#define ITER(x) { d[x+1] ^= t < d[x] ? d[x] ^ d[x+1] : 0; d[x] = t < d[x] ? t : d[x]; }
static inline void sort6_insertion_sort_unrolled_v2(int * d){
int t;
t = d[1]; ITER(0);
t = d[2]; ITER(1); ITER(0);
t = d[3]; ITER(2); ITER(1); ITER(0);
t = d[4]; ITER(3); ITER(2); ITER(1); ITER(0);
t = d[5]; ITER(4); ITER(3); ITER(2); ITER(1); ITER(0);
Эта сортировка вставки быстрее, чем у Даниэля Штуцбаха, и особенно хороша на графическом процессоре или компьютере с предикацией, потому что ITER может выполняться только с тремя инструкциями (против 4 для SWAP). Например, вот строка t = d[2]; ITER(1); ITER(0);
в сборке ARM:
MOV r6, r2
CMP r6, r1
MOVLT r2, r1
MOVLT r1, r6
CMP r6, r0
MOVLT r1, r0
MOVLT r0, r6
Для шести элементов сортировка вставки конкурирует с сетью сортировки (12 свопов против 15 итераций балансирует 4 команды/своп против 3 инструкций/итераций); сорт пузыря, конечно, медленнее. Но это будет не так, когда размер возрастает, поскольку сортировка вставки - это O (n ^ 2), а сортировочные сети - O (n log n).
Я портировал тестовый пакет на машину архитектуры PPC, которую я не могу идентифицировать (не нужно касаться кода, просто увеличивайте итерации теста, используйте 8 тестовых примеров, чтобы избежать загрязнения результатов с помощью модов и заменить специфичный x86 rdtsc ):
Прямой вызов функции библиотеки qsort: 101
Наивная реализация (сортировка вставки): 299
Вставка Сортировка (Daniel Stutzbach): 108
Вставка Сортировка развернута: 51
Сортировка сетей (Daniel Stutzbach): 26
Сортировка сетей (Paul R): 85
Сортировка сетей 12 с быстрой заменой: 117
Сортировка сетей 12 переупорядоченных свопов: 116
Порядок ранга: 56
Смена XOR может быть полезна в ваших функциях обмена.
void xorSwap (int *x, int *y) {
if (*x != *y) {
*x ^= *y;
*y ^= *x;
*x ^= *y;
}
}
Если это может привести к слишком большому расхождению в вашем коде, но если у вас есть гарантия, что все ваши ints уникальны, это может быть удобно.
x
и y
указывают на одно и то же место.
С нетерпением ждем попыток разобраться в этом и изучая эти примеры, но сначала некоторые тайминги от моего 1,5 ГГц PPC Powerbook G4 с 1 ГБ оперативной памяти DDR. (Я заимствовал аналогичный rdtsc-подобный таймер для PPC от http://www.mcs.anl.gov/~kazutomo/rdtsc.html для таймингов.) Я запускал программу несколько раз, и абсолютные результаты но последовательным самым быстрым тестом была "Insertion Sort (Daniel Stutzbach)", с "Insertion Sort Unrolled" - вторая секунда.
Здесь последний набор раз:
**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)** : 85
**Insertion Sort Unrolled** : 97
**Sorting Networks (Daniel Stutzbach)** : 457
**Sorting Networks (Paul R)** : 179
**Sorting Networks 12 with Fast Swap** : 238
**Sorting Networks 12 reordered Swap** : 236
**Rank Order** : 116
Вот мой вклад в этот поток: оптимизированный 1, 4 промежуточный shellsort для 6-членного int vector (valp), содержащий уникальные значения.
void shellsort (int *valp)
{
int c,a,*cp,*ip=valp,*ep=valp+5;
c=*valp; a=*(valp+4);if (c>a) {*valp= a;*(valp+4)=c;}
c=*(valp+1);a=*(valp+5);if (c>a) {*(valp+1)=a;*(valp+5)=c;}
cp=ip;
do
{
c=*cp;
a=*(cp+1);
do
{
if (c<a) break;
*cp=a;
*(cp+1)=c;
cp-=1;
c=*cp;
} while (cp>=valp);
ip+=1;
cp=ip;
} while (ip<ep);
}
На моем ноутбуке HP dv7-3010so с двухъядерным Athlon M300 @2 Ghz (память DDR2) он выполняется в 165 тактах. Это среднее значение, рассчитываемое по времени каждой уникальной последовательности (всего 6!/720). Скомпилирован в Win32 с использованием OpenWatcom 1.8. Цикл по существу является сортировкой вставки и составляет 16 инструкций/длиной 37 байт.
У меня нет 64-разрядной среды для компиляции.
Если сортировка вставки здесь достаточно конкурентоспособна, я бы рекомендовал попробовать shellsort. Я боюсь, что 6 элементов, вероятно, слишком малы, чтобы быть одним из лучших, но, возможно, стоит попробовать.
Пример кода, непроверенный, undebugged и т.д. Вы хотите настроить последовательность inc = 4 и inc - = 3, чтобы найти оптимальный (пример inc = 2, inc - = 1, например).
static __inline__ int sort6(int * d) {
char j, i;
int tmp;
for (inc = 4; inc > 0; inc -= 3) {
for (i = inc; i < 5; i++) {
tmp = a[i];
j = i;
while (j >= inc && a[j - inc] > tmp) {
a[j] = a[j - inc];
j -= inc;
}
a[j] = tmp;
}
}
}
Я не думаю, что это победит, но если кто-то задаст вопрос о сортировке 10 элементов, кто знает...
Согласно Википедии, это может быть даже объединено с сортировочными сетями: Pratt, V (1979). Оболочки и сортировочные сети (выдающиеся диссертации в области компьютерных наук). Garland. ISBN 0-824-04406-1
Я знаю, что я очень поздно, но мне было интересно экспериментировать с некоторыми другими решениями. Во-первых, я очистил эту пасту, скомпилировал ее и поместил в репозиторий. Я сохранил некоторые нежелательные решения как тупики, чтобы другие не пробовали. Среди них было мое первое решение, которое пыталось обеспечить однократное вычисление x1 > x2. После оптимизации он не быстрее других простых версий.
Я добавил циклическую версию сортировки ранжирования, так как мое собственное приложение этого исследования предназначено для сортировки 2-8 элементов, поэтому, поскольку существует переменное количество аргументов, необходим цикл. Вот почему я проигнорировал сетевые решения сортировки.
В тестовом коде не было проверено правильность обработки дубликатов, поэтому, хотя существующие решения были правильными, я добавил специальный код для тестового кода, чтобы убедиться, что дубликаты обработаны правильно.
Затем я написал сортировку вставки, которая полностью находится в регистре AVX. На моей машине он на 25% быстрее, чем другие сортировки вставки, но на 100% медленнее, чем ранжирование. Я сделал это исключительно для эксперимента и не ожидал, что это лучше из-за разветвления в сортировке вставки.
static inline void sort6_insertion_sort_avx(int* d) {
__m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], 0, 0);
__m256i index = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
__m256i shlpermute = _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6);
__m256i sorted = _mm256_setr_epi32(d[0], INT_MAX, INT_MAX, INT_MAX,
INT_MAX, INT_MAX, INT_MAX, INT_MAX);
__m256i val, gt, permute;
unsigned j;
// 8 / 32 = 2^-2
#define ITER(I) \
val = _mm256_permutevar8x32_epi32(src, _mm256_set1_epi32(I));\
gt = _mm256_cmpgt_epi32(sorted, val);\
permute = _mm256_blendv_epi8(index, shlpermute, gt);\
j = ffs( _mm256_movemask_epi8(gt)) >> 2;\
sorted = _mm256_blendv_epi8(_mm256_permutevar8x32_epi32(sorted, permute),\
val, _mm256_cmpeq_epi32(index, _mm256_set1_epi32(j)))
ITER(1);
ITER(2);
ITER(3);
ITER(4);
ITER(5);
int x[8];
_mm256_storeu_si256((__m256i*)x, sorted);
d[0] = x[0]; d[1] = x[1]; d[2] = x[2]; d[3] = x[3]; d[4] = x[4]; d[5] = x[5];
#undef ITER
}
Затем я написал сортировку порядка ранжирования с использованием AVX. Это соответствует скорости других решений ранга, но не быстрее. Проблема здесь в том, что я могу вычислять индексы с помощью AVX, а затем мне приходится составлять таблицу индексов. Это связано с тем, что вычисление основано на назначении, а не на основе исходного кода. См. Преобразование из индексов, основанных на источнике, в индексы на основе назначений
static inline void sort6_rank_order_avx(int* d) {
__m256i ror = _mm256_setr_epi32(5, 0, 1, 2, 3, 4, 6, 7);
__m256i one = _mm256_set1_epi32(1);
__m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], INT_MAX, INT_MAX);
__m256i rot = src;
__m256i index = _mm256_setzero_si256();
__m256i gt, permute;
__m256i shl = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 6, 6);
__m256i dstIx = _mm256_setr_epi32(0,1,2,3,4,5,6,7);
__m256i srcIx = dstIx;
__m256i eq = one;
__m256i rotIx = _mm256_setzero_si256();
#define INC(I)\
rot = _mm256_permutevar8x32_epi32(rot, ror);\
gt = _mm256_cmpgt_epi32(src, rot);\
index = _mm256_add_epi32(index, _mm256_and_si256(gt, one));\
index = _mm256_add_epi32(index, _mm256_and_si256(eq,\
_mm256_cmpeq_epi32(src, rot)));\
eq = _mm256_insert_epi32(eq, 0, I)
INC(0);
INC(1);
INC(2);
INC(3);
INC(4);
int e[6];
e[0] = d[0]; e[1] = d[1]; e[2] = d[2]; e[3] = d[3]; e[4] = d[4]; e[5] = d[5];
int i[8];
_mm256_storeu_si256((__m256i*)i, index);
d[i[0]] = e[0]; d[i[1]] = e[1]; d[i[2]] = e[2]; d[i[3]] = e[3]; d[i[4]] = e[4]; d[i[5]] = e[5];
}
Репо можно найти здесь: https://github.com/eyepatchParrot/sort6/
Этот вопрос становится довольно старым, но в действительности мне приходилось решать ту же проблему: быстрые агоримы для сортировки небольших массивов. Я подумал, что было бы неплохо поделиться своими знаниями. Когда я впервые начал использовать сортировочные сети, мне наконец удалось найти другие алгоритмы, для которых общее количество сравнений, выполняемых для сортировки каждой перестановки из 6 значений, было меньше, чем при сортировке сетей, и меньше, чем при сортировке вставки. Я не считал количество свопов; Я ожидаю, что это будет примерно эквивалентно (может быть, иногда немного выше).
Алгоритм sort6
использует алгоритм sort4
, который использует алгоритм sort3
. Вот реализация в некоторой легкой форме С++ (оригинал тяжелый шаблон, так что он может работать с любым итератором произвольного доступа и любой подходящей функцией сравнения).
Следующий алгоритм представляет собой развернутую сортировку вставки. Когда необходимо выполнить два свопа (6 назначений), вместо этого он использует 4 назначения:
void sort3(int* array)
{
if (array[1] < array[0]) {
if (array[2] < array[0]) {
if (array[2] < array[1]) {
std::swap(array[0], array[2]);
} else {
int tmp = array[0];
array[0] = array[1];
array[1] = array[2];
array[2] = tmp;
}
} else {
std::swap(array[0], array[1]);
}
} else {
if (array[2] < array[1]) {
if (array[2] < array[0]) {
int tmp = array[2];
array[2] = array[1];
array[1] = array[0];
array[0] = tmp;
} else {
std::swap(array[1], array[2]);
}
}
}
}
Он выглядит немного сложным, поскольку сортировка имеет более или менее одну ветвь для каждой возможной перестановки массива, используя 2 ~ 3 сравнения и не более 4 назначений для сортировки трех значений.
Этот вызов вызывает sort3
, затем выполняет развернутую сортировку вставки с последним элементом массива:
void sort4(int* array)
{
// Sort the first 3 elements
sort3(array);
// Insert the 4th element with insertion sort
if (array[3] < array[2]) {
std::swap(array[2], array[3]);
if (array[2] < array[1]) {
std::swap(array[1], array[2]);
if (array[1] < array[0]) {
std::swap(array[0], array[1]);
}
}
}
}
Этот алгоритм выполняет от 3 до 6 сравнений и не более 5 свопов. Легко развернуть сортировку вставки, но мы будем использовать другой алгоритм для последнего сортировки...
В этом случае используется развернутая версия того, что я назвал двойной сортировкой вставки. Название не так уж и велико, но это довольно описательно, вот как это работает:
После свопа первый элемент всегда меньше последнего, что означает, что при вставке их в отсортированную последовательность не будет больше N сравнений, чтобы вставить два элемента в худшем случае: например, если первый элемент был вставлен в третью позицию, то последний не может быть вставлен ниже четвертой позиции.
void sort6(int* array)
{
// Sort everything but first and last elements
sort4(array+1);
// Switch first and last elements if needed
if (array[5] < array[0]) {
std::swap(array[0], array[5]);
}
// Insert first element from the front
if (array[1] < array[0]) {
std::swap(array[0], array[1]);
if (array[2] < array[1]) {
std::swap(array[1], array[2]);
if (array[3] < array[2]) {
std::swap(array[2], array[3]);
if (array[4] < array[3]) {
std::swap(array[3], array[4]);
}
}
}
}
// Insert last element from the back
if (array[5] < array[4]) {
std::swap(array[4], array[5]);
if (array[4] < array[3]) {
std::swap(array[3], array[4]);
if (array[3] < array[2]) {
std::swap(array[2], array[3]);
if (array[2] < array[1]) {
std::swap(array[1], array[2]);
}
}
}
}
}
Мои тесты при каждой перестановке из 6 значений показывают, что эти алгоритмы всегда выполняют между 6 и 13 сравнениями. Я не вычислял количество выполняемых свопов, но я не ожидаю, что в худшем случае он будет выше 11.
Я надеюсь, что это поможет, даже если этот вопрос больше не может представлять собой настоящую проблему:)
РЕДАКТИРОВАТЬ: после того, как он поместит его в предоставленный ориентир, он будет медленнее, чем большинство интересных альтернатив. Он имеет тенденцию работать немного лучше, чем развернутая сортировка вставки, но это в значительной степени. В принципе, это не лучший вид для целых чисел, но может быть интересен для типов с дорогостоящей операцией сравнения.
Я знаю, что это старый вопрос.
Но я просто написал другое решение, которое хочу поделиться. Используя ничего, кроме вложенного MIN MAX,
Это не быстро, так как он использует 114 из каждого,
мог бы уменьшить его до 75, просто так вот → pastebin
Но тогда это не чисто минмакс больше.
Что может работать min/max для нескольких целых чисел одновременно с AVX
#include <stdio.h>
static __inline__ int MIN(int a, int b){
int result =a;
__asm__ ("pminsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ int MAX(int a, int b){
int result = a;
__asm__ ("pmaxsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ unsigned long long rdtsc(void){
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" :
"=A" (x));
return x;
}
#define MIN3(a, b, c) (MIN(MIN(a,b),c))
#define MIN4(a, b, c, d) (MIN(MIN(a,b),MIN(c,d)))
static __inline__ void sort6(int * in) {
const int A=in[0], B=in[1], C=in[2], D=in[3], E=in[4], F=in[5];
in[0] = MIN( MIN4(A,B,C,D),MIN(E,F) );
const int
AB = MAX(A, B),
AC = MAX(A, C),
AD = MAX(A, D),
AE = MAX(A, E),
AF = MAX(A, F),
BC = MAX(B, C),
BD = MAX(B, D),
BE = MAX(B, E),
BF = MAX(B, F),
CD = MAX(C, D),
CE = MAX(C, E),
CF = MAX(C, F),
DE = MAX(D, E),
DF = MAX(D, F),
EF = MAX(E, F);
in[1] = MIN4 (
MIN4( AB, AC, AD, AE ),
MIN4( AF, BC, BD, BE ),
MIN4( BF, CD, CE, CF ),
MIN3( DE, DF, EF)
);
const int
ABC = MAX(AB,C),
ABD = MAX(AB,D),
ABE = MAX(AB,E),
ABF = MAX(AB,F),
ACD = MAX(AC,D),
ACE = MAX(AC,E),
ACF = MAX(AC,F),
ADE = MAX(AD,E),
ADF = MAX(AD,F),
AEF = MAX(AE,F),
BCD = MAX(BC,D),
BCE = MAX(BC,E),
BCF = MAX(BC,F),
BDE = MAX(BD,E),
BDF = MAX(BD,F),
BEF = MAX(BE,F),
CDE = MAX(CD,E),
CDF = MAX(CD,F),
CEF = MAX(CE,F),
DEF = MAX(DE,F);
in[2] = MIN( MIN4 (
MIN4( ABC, ABD, ABE, ABF ),
MIN4( ACD, ACE, ACF, ADE ),
MIN4( ADF, AEF, BCD, BCE ),
MIN4( BCF, BDE, BDF, BEF )),
MIN4( CDE, CDF, CEF, DEF )
);
const int
ABCD = MAX(ABC,D),
ABCE = MAX(ABC,E),
ABCF = MAX(ABC,F),
ABDE = MAX(ABD,E),
ABDF = MAX(ABD,F),
ABEF = MAX(ABE,F),
ACDE = MAX(ACD,E),
ACDF = MAX(ACD,F),
ACEF = MAX(ACE,F),
ADEF = MAX(ADE,F),
BCDE = MAX(BCD,E),
BCDF = MAX(BCD,F),
BCEF = MAX(BCE,F),
BDEF = MAX(BDE,F),
CDEF = MAX(CDE,F);
in[3] = MIN4 (
MIN4( ABCD, ABCE, ABCF, ABDE ),
MIN4( ABDF, ABEF, ACDE, ACDF ),
MIN4( ACEF, ADEF, BCDE, BCDF ),
MIN3( BCEF, BDEF, CDEF )
);
const int
ABCDE= MAX(ABCD,E),
ABCDF= MAX(ABCD,F),
ABCEF= MAX(ABCE,F),
ABDEF= MAX(ABDE,F),
ACDEF= MAX(ACDE,F),
BCDEF= MAX(BCDE,F);
in[4]= MIN (
MIN4( ABCDE, ABCDF, ABCEF, ABDEF ),
MIN ( ACDEF, BCDEF )
);
in[5] = MAX(ABCDE,F);
}
int main(int argc, char ** argv) {
int d[6][6] = {
{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 (int i = 0; i < 6; i++) {
sort6(d[i]);
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
for (int i = 0; i < 6; i++) {
printf("d%d : %d %d %d %d %d %d\n", i,
d[i][0], d[i][1], d[i][2],
d[i][3], d[i][4], d[i][5]);
}
}
EDIT:
Ранжирование заказа, вдохновленное Рексом Керром,
Гораздо быстрее, чем беспорядок выше
static void sort6(int *o) {
const int
A=o[0],B=o[1],C=o[2],D=o[3],E=o[4],F=o[5];
const unsigned char
AB = A>B, AC = A>C, AD = A>D, AE = A>E,
BC = B>C, BD = B>D, BE = B>E,
CD = C>D, CE = C>E,
DE = D>E,
a = AB + AC + AD + AE + (A>F),
b = 1 - AB + BC + BD + BE + (B>F),
c = 2 - AC - BC + CD + CE + (C>F),
d = 3 - AD - BD - CD + DE + (D>F),
e = 4 - AE - BE - CE - DE + (E>F);
o[a]=A; o[b]=B; o[c]=C; o[d]=D; o[e]=E;
o[15-a-b-c-d-e]=F;
}
Я обнаружил, что, по крайней мере, на моей системе, функции sort6_iterator()
и sort6_iterator_local()
, определенные ниже, выполнялись как минимум так быстро, а часто заметно быстрее, чем предыдущий текущий держатель:
#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)
template<class IterType>
inline void sort6_iterator(IterType it)
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
const auto b = MAX(*(it + x), *(it + y)); \
*(it + x) = a; *(it + 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
}
Я передал эту функцию итератору std::vector
в моем коде времени. Я подозреваю, что использование итераторов дает g++ определенные заверения в том, что может и не может случиться с памятью, на которую ссылается итератор, чего у него иначе не было бы, и именно эти заверения позволяют g++ лучше оптимизировать код сортировки (который if Я правильно помню, также является причиной того, почему так много алгоритмов std
, таких как std::sort()
, как правило, имеют такую неприличную хорошую производительность). Однако, хотя время я заметил, что контекст (т.е. Окружающий код), в котором был выполнен вызов функции сортировки, оказал значительное влияние на производительность, что, вероятно, связано с тем, что функция была встроена, а затем оптимизирована. Например, если программа была достаточно простой, то обычно не было большой разницы в производительности между передачей функции сортировки указателем и передачей им итератора; в противном случае использование итераторов обычно приводило к значительно более высокой производительности и никогда (по моему опыту, по крайней мере, по крайней мере) какой-либо заметно худшей производительности. Я подозреваю, что это может быть связано с тем, что g++ может глобально оптимизировать достаточно простой код.
Кроме того, sort6_iterator()
иногда (опять же, в зависимости от контекста, в котором вызывается функция), последовательно превосходит следующую функцию сортировки:
template<class IterType>
inline void sort6_iterator_local(IterType it)
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
const auto b = MAX(data##x, data##y); \
data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a) auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;
DD2(1,2) SWAP(1, 2)
DD2(4,5) SWAP(4, 5)
DD1(0) SWAP(0, 2)
DD1(3) SWAP(3, 5)
SWAP(0, 1) SWAP(3, 4)
SWAP(1, 4) SWAP(0, 3) CB(0)
SWAP(2, 5) CB(5)
SWAP(1, 3) CB(1)
SWAP(2, 4) CB(4)
SWAP(2, 3) CB(2) CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}
Обратите внимание, что определение SWAP()
, как следует, иногда приводит к немного лучшей производительности, хотя в большинстве случаев это приводит к немного худшей производительности или незначительной разнице в производительности.
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
data##y = MAX(data##x, data##y); \
data##x = a; }
Если вам нужен алгоритм сортировки, который gcc -O3 последовательно оптимизирует, то в зависимости от того, как вы передаете вход, попробуйте один из следующих двух алгоритмов:
template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a) register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a) *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
DD2(1,2) SORT2(1,2)
DD2(4,5) SORT2(4,5)
DD1(0) SORT2(0,2)
DD1(3) SORT2(3,5)
SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
SORT2(1,4) SORT2(0,3) CB1(0)
SORT2(2,4) CB1(4)
SORT2(1,3) CB1(1)
SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}
или
template<class T> inline void sort6(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5) {
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a) register auto data##a=e##a;
#define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b;
#define CB1(a) e##a=data##a;
#define CB2(a,b) e##a=data##a;e##b=data##b;
DD2(1,2) SORT2(1,2)
DD2(4,5) SORT2(4,5)
DD1(0) SORT2(0,2)
DD1(3) SORT2(3,5)
SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
SORT2(1,4) SORT2(0,3) CB1(0)
SORT2(2,4) CB1(4)
SORT2(1,3) CB1(1)
SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}
Причина использования ключевого слова register
заключается в том, что это одно из немногих случаев, когда вы знаете, что эти значения нужны для регистров. Без register
компилятор будет определять это большую часть времени, но иногда это не так. Использование ключевого слова register
помогает решить эту проблему. Обычно, однако, не используйте ключевое слово register
, поскольку он скорее замедляет ваш код, чем ускоряет его.
Также обратите внимание на использование шаблонов. Это делается специально, так как даже с ключевым словом inline
функции шаблона, как правило, гораздо более агрессивно оптимизируются gcc, чем функции ваниль C (это связано с тем, что gcc необходимо иметь дело с указателями функций для функций ваниль C, но не с шаблоном функции).
Я считаю, что есть две части вашего вопроса.
Я бы не стал слишком беспокоиться об освобождении конвейеров (предполагая текущий x86): предсказание ветки прошло долгий путь. Я бы беспокоился о том, что код и данные вписываются в одну строку кэша (возможно, два для кода). После того, как выборки задержек будут освежающе низкими, что компенсирует любой ларек. Это также означает, что ваш внутренний цикл будет, может быть, десятью инструкциями или так, что правильно там, где это должно быть (в моем алгоритме сортировки есть две разные внутренние петли, они - 10 команд /22 байта и 9/22 длинны соответственно). Предполагая, что код не содержит никаких div, вы можете быть уверены, что он будет ослепительно быстрым.
Возможно, я опаздываю на вечеринку, но, по крайней мере, мой вклад - это новый подход.
swap
будет выше (irt стоимость compare
)SWAP()
двух элементов, циклы преследуются, требуется только одна временная и одна (register- > register) swap (new < - old).Обновление: немного изменил код, некоторые люди используют компиляторы С++ для компиляции кода C...
#include <stdio.h>
#if WANT_CHAR
typedef signed char Dif;
#else
typedef signed int Dif;
#endif
static int walksort (int *arr, int cnt);
static void countdifs (int *arr, Dif *dif, int cnt);
static void calcranks(int *arr, Dif *dif);
int wsort6(int *arr);
void do_print_a(char *msg, int *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
fprintf(stderr, " %3d", *arr);
}
fprintf(stderr,"\n");
}
void do_print_d(char *msg, Dif *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
fprintf(stderr, " %3d", (int) *arr);
}
fprintf(stderr,"\n");
}
static void inline countdifs (int *arr, Dif *dif, int cnt)
{
int top, bot;
for (top = 0; top < cnt; top++ ) {
for (bot = 0; bot < top; bot++ ) {
if (arr[top] < arr[bot]) { dif[top]--; dif[bot]++; }
}
}
return ;
}
/* Copied from RexKerr ... */
static void inline calcranks(int *arr, Dif *dif){
dif[0] = (arr[0]>arr[1])+(arr[0]>arr[2])+(arr[0]>arr[3])+(arr[0]>arr[4])+(arr[0]>arr[5]);
dif[1] = -1+ (arr[1]>=arr[0])+(arr[1]>arr[2])+(arr[1]>arr[3])+(arr[1]>arr[4])+(arr[1]>arr[5]);
dif[2] = -2+ (arr[2]>=arr[0])+(arr[2]>=arr[1])+(arr[2]>arr[3])+(arr[2]>arr[4])+(arr[2]>arr[5]);
dif[3] = -3+ (arr[3]>=arr[0])+(arr[3]>=arr[1])+(arr[3]>=arr[2])+(arr[3]>arr[4])+(arr[3]>arr[5]);
dif[4] = -4+ (arr[4]>=arr[0])+(arr[4]>=arr[1])+(arr[4]>=arr[2])+(arr[4]>=arr[3])+(arr[4]>arr[5]);
dif[5] = -(dif[0]+dif[1]+dif[2]+dif[3]+dif[4]);
}
static int walksort (int *arr, int cnt)
{
int idx, src,dst, nswap;
Dif difs[cnt];
#if WANT_REXK
calcranks(arr, difs);
#else
for (idx=0; idx < cnt; idx++) difs[idx] =0;
countdifs(arr, difs, cnt);
#endif
calcranks(arr, difs);
#define DUMP_IT 0
#if DUMP_IT
do_print_d("ISteps ", difs, cnt);
#endif
nswap = 0;
for (idx=0; idx < cnt; idx++) {
int newval;
int step,cyc;
if ( !difs[idx] ) continue;
newval = arr[idx];
cyc = 0;
src = idx;
do {
int oldval;
step = difs[src];
difs[src] =0;
dst = src + step;
cyc += step ;
if(dst == idx+1)idx=dst;
oldval = arr[dst];
#if (DUMP_IT&1)
fprintf(stderr, "[Nswap=%d] Cyc=%d Step=%2d Idx=%d Old=%2d New=%2d #### Src=%d Dst=%d[%2d]->%2d <-- %d\n##\n"
, nswap, cyc, step, idx, oldval, newval
, src, dst, difs[dst], arr[dst]
, newval );
do_print_a("Array ", arr, cnt);
do_print_d("Steps ", difs, cnt);
#endif
arr[dst] = newval;
newval = oldval;
nswap++;
src = dst;
} while( cyc);
}
return nswap;
}
/*************/
int wsort6(int *arr)
{
return walksort(arr, 6);
}
Сортировка 4 элементов с использованием cmp == 0. Числа cmp ~ 4.34 (FF native имеют ~ 4.52), но занимают 3 раза, чем слияние списков. Но лучше, чем CMP операций, если у вас большие цифры или большой текст. Изменить: исправлена ошибка
Онлайн-тест http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm
function sort4DG(cmp,start,end,n) // sort 4
{
var n = typeof(n) !=='undefined' ? n : 1;
var cmp = typeof(cmp) !=='undefined' ? cmp : sortCompare2;
var start = typeof(start)!=='undefined' ? start : 0;
var end = typeof(end) !=='undefined' ? end : arr[n].length;
var count = end - start;
var pos = -1;
var i = start;
var cc = [];
// stabilni?
cc[01] = cmp(arr[n][i+0],arr[n][i+1]);
cc[23] = cmp(arr[n][i+2],arr[n][i+3]);
if (cc[01]>0) {swap(n,i+0,i+1);}
if (cc[23]>0) {swap(n,i+2,i+3);}
cc[12] = cmp(arr[n][i+1],arr[n][i+2]);
if (!(cc[12]>0)) {return n;}
cc[02] = cc[01]==0 ? cc[12] : cmp(arr[n][i+0],arr[n][i+2]);
if (cc[02]>0)
{
swap(n,i+1,i+2); swap(n,i+0,i+1); // bubble last to top
cc[13] = cc[23]==0 ? cc[12] : cmp(arr[n][i+1],arr[n][i+3]);
if (cc[13]>0)
{
swap(n,i+2,i+3); swap(n,i+1,i+2); // bubble
return n;
}
else {
cc[23] = cc[23]==0 ? cc[12] : (cc[01]==0 ? cc[30] : cmp(arr[n][i+2],arr[n][i+3])); // new cc23 | c03 //repaired
if (cc[23]>0)
{
swap(n,i+2,i+3);
return n;
}
return n;
}
}
else {
if (cc[12]>0)
{
swap(n,i+1,i+2);
cc[23] = cc[23]==0 ? cc[12] : cmp(arr[n][i+2],arr[n][i+3]); // new cc23
if (cc[23]>0)
{
swap(n,i+2,i+3);
return n;
}
return n;
}
else {
return n;
}
}
return n;
}
Попробуйте сортировать отсортированный список.:) Используйте два массива. Самый быстрый для малого и большого массива.
Если вы соглашаетесь, вы только проверяете, где вставить. Другие большие значения, которые вам не нужно сравнивать (cmp = a-b > 0).
Для 4 чисел вы можете использовать систему 4-5 cmp (~ 4.6) или 3-6 cmp (~ 4.9). Сортировка пузырьков составляет 6 см (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 code
function sortListMerge_2a(cmp)
{
var step, stepmax, tmp, a,b,c, i,j,k, m,n, cycles;
var start = 0;
var end = arr_count;
//var str = '';
cycles = 0;
if (end>3)
{
stepmax = ((end - start + 1) >> 1) << 1;
m = 1;
n = 2;
for (step=1;step<stepmax;step<<=1) //bounds 1-1, 2-2, 4-4, 8-8...
{
a = start;
while (a<end)
{
b = a + step;
c = a + step + step;
b = b<end ? b : end;
c = c<end ? c : end;
i = a;
j = b;
k = i;
while (i<b && j<c)
{
if (cmp(arr[m][i],arr[m][j])>0)
{arr[n][k] = arr[m][j]; j++; k++;}
else {arr[n][k] = arr[m][i]; i++; k++;}
}
while (i<b)
{arr[n][k] = arr[m][i]; i++; k++;
}
while (j<c)
{arr[n][k] = arr[m][j]; j++; k++;
}
a = c;
}
tmp = m; m = n; n = tmp;
}
return m;
}
else
{
// sort 3 items
sort10(cmp);
return m;
}
}
Вот три типичных метода сортировки, которые представляют три разных класса алгоритмов сортировки:
Insertion Sort: Θ(n^2)
Heap Sort: Θ(n log n)
Count Sort: Θ(3n)
Но зайдите обсуждение Стефана Нельсона о самом быстром алгоритме сортировки?, где он обсуждает решение, которое переходит на O(n log log n)
.. проверьте его реализация в C
Этот алгоритм полулинейной сортировки был представлен в 1995 году:
а. Андерссон, Т. Хагеруп, С. Нильссон и Р. Раман. Сортировка по линейному время? В материалах 27-го ежегодного симпозиума ACM по теории Вычисление, страницы 427-436, 1995.
Ну, если это всего 6 элементов, и вы можете использовать parallelism, хотите минимизировать условное разветвление и т.д. Почему вы не генерируете все комбинации и не проверяете порядок? Я бы рискнул, что в некоторых архитектурах это может быть довольно быстро (до тех пор, пока у вас есть память prealocated)
vec3
или аналогичные вместо массива, так что вы можете использовать swizzling для сортировки.