Найшвидший сорт фіксованої довжини 6 int масив


401

Відповідаючи на ще одне запитання про переповнення стека (на це ), я натрапив на цікаву підпроблему. Який найшвидший спосіб сортувати масив із 6 цілих чисел?

Оскільки питання дуже низький:

  • ми не можемо припустити, що бібліотеки доступні (і сам виклик має свою вартість), лише звичайний C
  • щоб уникнути спорожнення конвеєра інструкцій (що має дуже високу вартість), ми, ймовірно, повинні мінімізувати розгалуження, стрибки та будь-який інший вид контролю потоку переривання (як, наприклад, приховані за пунктами послідовності в &&або ||).
  • приміщення обмежене і мінімізація регістрів, а використання пам’яті є проблемою, в ідеалі на місці сортування, мабуть, найкраще.

Дійсно, це питання є різновидом Golf, де мета - не мінімізувати довжину джерела, а час виконання. Я називаю це «код Zening», використовуваним в назві книги Дзна оптимізація коди по Абраш і його продовжень .

Щодо того, чому це цікаво, є кілька шарів:

  • приклад простий і легкий для розуміння та вимірювання, не так багато навичок роботи на С
  • він показує ефекти вибору хорошого алгоритму для проблеми, але також ефекти компілятора та базового обладнання.

Ось моя референтна (наївна, не оптимізована) реалізація та мій тестовий набір.

#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
  • Сортування мереж (Пол Р.): 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
  • Сортування мереж (Пол Р.): 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 рази) менш ефективним, ніж простий, який використовується, якщо в архітектурі КПП.

Виклик бібліотеки 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 і 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 за допомогою простого заміни

Через рік після первинної публікації Стейнар Х. Гендерсон запропонував нам не намагатися перехитрити компілятор і зберегти простий код свопу. Це дійсно гарна ідея, оскільки отриманий код на 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 цикли. Я називаю це напрочуд швидким. Можливі інші вдосконалення?


2
Чи є у вас обмеження щодо вкладень? Наприклад, чи можемо ми припустити, що для будь-яких 2 x, y x-yі x+yне спричиняє переливу чи переповнення?
Матьє М.

3
Вам слід спробувати поєднати мою мережу сортування з 12 замінами з функцією свопу без відгалужень Павла. Його рішення передає всі параметри як окремі елементи на стеку замість одного вказівника на масив. Це також може змінити значення.
Даніель Штуцбах

2
Зауважте, що правильна реалізація rdtsc на 64-розрядному відбувається __asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");тому, що rdtsc ставить відповідь у EDX: EAX, тоді як GCC очікує її в єдиному 64-бітному регістрі. Ви можете бачити помилку, зібравши на -O3. Також дивіться нижче мій коментар до Пола R про швидший SWAP.
Паоло Бонзіні

3
@Tyler: Як ви реалізуєте це на рівні складання без гілки?
Лорен Печтел

4
@Loren: CMP EAX, EBX; SBB EAX, EAXпоставить або 0, або 0xFFFFFFFF EAXзалежно від того EAX, більший чи менший, ніж EBXвідповідно. SBBє "відняти з позикою", аналог ADC("додати з нести"); біт статусу, на який ви посилаєтесь, - це біт перенесення. Потім я знову пам’ятаю це ADCі SBBмав жахливу затримку та пропускну здатність на Pentium 4 vs. ADDта SUB, і все ще був удвічі повільнішим на основних процесорах. Починаючи з 80386, існують також інструкції з SETccумовного зберігання та CMOVccумовного переміщення, але вони також повільні.
j_random_hacker

Відповіді:


162

Для будь-якої оптимізації завжди найкраще тестувати, тестувати, тестувати. Я б спробував принаймні сортувати мережі та сортувати вставки. Якби я робив ставку, я б поклав свої гроші на вставку на основі минулого досвіду.

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

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

Ось реалізація сортування вставки:

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
}

9
+1: приємно, ви робили це з 12 обмінів, а не з 13 в моїй кодованій та емпірично отриманій мережі вище. Я хотів би дати вам ще +1, якщо зможу за посилання на сайт, який генерує для вас мережі - тепер закладено в закладки.
Пол Р

9
Це фантастична ідея для функції сортування загального призначення, якщо ви очікуєте, що більшість запитів мають масиви невеликого розміру. Використовуйте оператор перемикання для випадків, які ви хочете оптимізувати, використовуючи цю процедуру; нехай у випадку за замовчуванням використовується функція сортування бібліотеки.
Марк Рансом

5
@Mark Хороша функція сортування бібліотеки вже матиме швидкий шлях для малих масивів. Багато сучасних бібліотек використовуватимуть рекурсивний QuickSort або MergeSort, який переходить на InsertionSort після повторного переходу до n < SMALL_CONSTANT.
Даніель Штуцбах

3
@Mark Ну, для функції сортування бібліотеки С потрібно вказати операцію порівняння за допомогою функції переносу функцій. Витрати на виклик функції для кожного порівняння величезні. Зазвичай, це все-таки найчистіший шлях, оскільки це рідко є критичним шляхом у програмі. Однак, якщо це критичний шлях, ми дійсно можемо сортувати набагато швидше, якщо знаємо, що ми сортуємо цілі числа та саме 6 з них. :)
Даніель Штуцбах

7
@tgwh: XOR своп майже завжди погана ідея.
Пол Р

63

Ось реалізація за допомогою сортування мереж :

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 ISA мають векторні інструкції min / max), а також реалізація GPU (наприклад, CUDA - будучи безрозгалуженим, немає проблем з розбіжністю warp тощо).

Дивіться також: Швидка реалізація алгоритму для сортування дуже невеликого списку


1
Деякі хакі для min / max: graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax
Rubys

1
@Paul: у реальному контексті використання CUDA це, безумовно, найкраща відповідь. Я перевірю, чи він також є (і скільки) в golf x64 контексті, і опублікую результат.
kriss

1
Sort3було б швидше (у більшості архітектур, все одно), якби ви зазначили, що (a+b+c)-(min+max)це центральне число.
Рекс Керр

1
@Rex: Я бачу - це добре виглядає. Для архітектур SIMD, таких як AltiVec і SSE, це було б однакова кількість циклів інструкцій (max і min - це інструкції для одного циклу, такі як додавання / віднімання), але для нормального скалярного процесора ваш метод виглядає краще.
Пол Р

2
Якщо я дозволю GCC оптимізувати хв з умовними зрушують я отримую 33% прискорення в: #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], оскільки він дає дещо гірші показники, але він майже в шумі.
Паоло Бонзіні

45

Оскільки цілі числа, а порівняння швидко, чому б не обчислити порядок ранжування кожного безпосередньо:

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];
}

@Rex: з gcc -O1 це нижче 1000 циклів, досить швидко, але повільніше, ніж сортування мережі. Будь-яка ідея поліпшити код? Можливо, якби ми могли уникнути копіювання масиву ...
kriss

@kriss: Це швидше, ніж мережа сортування для мене з -O2. Чи є якась причина, чому -O2 не в порядку, або це повільніше для вас на -O2? Може, це різниця в машинній архітектурі?
Рекс Керр

1
@Rex: вибачте, я пропустив шаблон> vs> = з першого погляду. Це працює в кожному випадку.
kriss

3
@kriss: Ага. Це не зовсім дивно - навколо них плаває дуже багато змінних, і їх потрібно ретельно упорядкувати та кешувати в регістрах тощо.
Рекс Керр

2
@SSpoke 0+1+2+3+4+5=15Оскільки одного з них не вистачає, 15 мінус суми решти прибутків відсутній
Glenn Teitelbaum

35

Схоже, я пішов на вечірку на рік, але ось ми їдемо ...

Дивлячись на збірку, що генерується gcc 4.5.2, я помітив, що завантаження та зберігання робляться для кожної заміни, яка справді не потрібна. Було б краще завантажити 6 значень у регістри, сортувати їх і зберігати їх назад у пам'яті. Я наказав вантажі в магазинах бути максимально наближеними до там, де реєстри спочатку потрібні та останні використовуються. Я також використовував SWAP макрос Steinar H. Gunderson. Оновлення: я перейшов на SWAP-макрос Паоло Бонзіні, який 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оптимізація не буде контрпродуктивною.
cdunn2001

1
@ cdunn2001 Я просто перевірив це, я не бачу поліпшення (за винятком кількох циклів при -O0 і -Os). Дивлячись на asm, здається, що gcc вже вдалося з'ясувати використання регістрів та усунення виклику memcpy.
Кевін Сток

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

1
У вашому коді все ще використовується своп Гундерсона, мій був би #define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }.
Паоло Бонзіні

@ Паоло Бонзіні: Так, я маю намір додати тестовий випадок із вашим, просто ще не встиг. Але я уникаю вбудованої збірки.
kriss

15

Я натрапив на це питання від 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 для виклику функції з коду С:

void simd_sort_6(char *values);

Цікаво буде порівнювати ваші пропозиції з іншими пропозиціями на рівні складання. Порівняні показники реалізації не включають їх. Використання SSE все одно добре звучить.
kriss

Іншим напрямком майбутніх досліджень буде застосування нових інструкцій Intel AVX до цієї проблеми. Більші 256-бітні вектори досить великі, щоб вмістити 8 DWORD.
Джо Крівелло

1
Замість цього pxor / pinsrd xmm4, mem, 0просто використовуйте movd!
Пітер Кордес

14

Тестовий код досить поганий; він переповнює початковий масив (чи не люди тут читають попередження компілятора?), 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).


Гаразд, тестовий код поганий. Сміливо вдосконалюйте його. І так, ви можете використовувати код складання. Чому б не пройти весь шлях і повністю кодувати його за допомогою асемблера x86? Це може бути трохи менш портативно, але чому це турбувати?
kriss

Дякую за те, що помітили переповнення масиву, я його виправив. Інші люди, можливо, не помітили цього, оскільки натиснувши на посилання, щоб скопіювати / вставити код, де немає переповнення.
kriss

4
Вам фактично навіть не потрібен асемблер; якщо ви просто кинете всі хитрі трюки, GCC розпізнає послідовність і вставить для вас умовні рухи: #define min (a, b) ((a <b)? a: b) #define max (a, b) ( (a <b)? b: a) #define SWAP (x, y) {int a = min (d [x], d [y]); int b = max (d [x], d [y]); d [x] = a; d [y] = b; } Виходить, можливо, на кілька відсотків повільніше, ніж вбудований варіант асм, але це важко сказати, враховуючи відсутність належного бенчмаркінгу.
Стейнар Х. Гундерсон

3
… І нарешті, якщо ваші цифри плавають, і вам не потрібно турбуватися про NaN тощо., GCC може перетворити це в інструкції SSE minss / maxss, що ще ~ 25% швидше. Мораль: відмовтеся від хитромудрих хитрощів і дозвольте компілятору виконати свою роботу. :-)
Стейнар Х. Гундерсон

13

Хоча мені дуже подобається наданий макрос swap:

#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 [y] отримує максимум, який дорівнює x ^ (загальна субекспресія).
Кевін Сток

Я помітив те саме; Я думаю, що для вашої реалізації правильним ви хочете d[x]замість x(те саме y), а d[y] < d[x]для нерівності тут (так, відмінний від коду min / max).
Тайлер

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

12

Ніколи не оптимізуйте min / max без бенчмаркінгу та перегляду фактичної збірки, створеної компілятором. Якщо я дозволю GCC оптимізувати хв за допомогою умовних інструкцій з переміщення, я отримую 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 у циклі тесту - 280 проти 420 циклів). Зробити макс з?: Майже так само, майже втратити шум, але вище сказане трохи швидше. Цей 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 можна виконати лише з 3 інструкціями (проти 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).


1
Більш-менш пов'язане: я подав звіт до GCC, щоб він міг здійснити оптимізацію безпосередньо в компіляторі. Не впевнений, що це буде зроблено, але принаймні ви можете прослідкувати, як воно розвивається.
Морвен

11

Я переніс набір тестів на машину архітектури КПП, яку я не можу ідентифікувати (не потрібно було торкатися коду, просто збільште ітерації тесту, використовуйте 8 тестових випадків, щоб уникнути забруднення результатів модами та замінити x86 на конкретний rdtsc):

Прямий дзвінок до функції бібліотеки qsort : 101

Наївна реалізація (сортування вставки) : 299

Сортування вставки (Даніель Штуцбах) : 108

Сортування вставки без розгортання : 51

Сортувальні мережі (Даніель Штуцбах) : 26

Сортування мереж (Пол Р.) : 85

Сортування мереж 12 за допомогою швидкої заміни : 117

Сортування мереж 12 упорядкований своп : 116

Порядок звання : 56


1
Дійсно цікаво. Схоже, що безгалузевий своп - це погана ідея для КПП. Це також може бути ефектом компілятора. Який із них використовувався?
kriss

Його гілка компілятора gcc - мінімальна, максимальна логіка, ймовірно, не є безрозгалуженою - я буду перевіряти розбирання і повідомляти вам, але, якщо компілятор не досить розумний, включаючи щось на зразок x <y без, якщо все-таки стає гілкою - на x86 / x64 інструкція CMOV може цього уникнути, але немає такої інструкції для фіксованих точок на PPC, тільки плаває. Я б міг заперечувати з цим завтра і повідомляти вам - я пам’ятаю, що в джерелі Winamp AVS був набагато простіший min / max без відгалужень, але iirc це був лише для поплавців - але це може стати гарним початком до справді нерозгалуженого підходу.
jheriko

4
Ось Позаофісне хв / макс для PPC з підписаними входами: 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 до всіх елементів на початку і знову відняти його в кінці, а потім працювати так, ніби вони не були підписані.
Паоло Бонзіні

7

XOR своп може бути корисним у ваших функціях заміни.

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

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


1
xor swap працює і для рівних значень ... x ^ = y встановлює x до 0, y ^ = x залишає y як y (== x), x ^ = y встановлює x в y
jheriko

11
Коли це не працює, це коли xі yвказувати на те саме місце.
варення

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

5

З нетерпінням чекаю на те, щоб спробувати свої сили, і навчитися на цих прикладах, але спочатку кілька моментів з моєї 1,5 ГГц PPC Powerbook G4 w / 1 GB DDR RAM. (Я запозичив аналогічний таймер rdtsc для PPC у http://www.mcs.anl.gov/~kazutomo/rdtsc.html для таймінгів.) Я запускав програму кілька разів, і абсолютні результати змінювались, але стабільно найшвидшим випробуванням було "Сортування вставки (Даніель Штуцбах)", а "Вставити сортування без розгортання" близько секунди.

Ось останній набір разів:

**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

4

Ось мій внесок у цю нитку: оптимізований оболонку розриву 1, 4 для 6-членного int-вектора (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-бітного середовища для компіляції.


приємно. Я додам його до більш тривалого тесту
kriss

3

Якщо сортування вставки є досить конкурентоспроможним, я рекомендую спробувати шкаралупу. Боюся, що 6 елементів - це, мабуть, занадто мало, щоб стати одними з найкращих, але, можливо, варто спробувати.

Приклад коду, неперевірений, неналагоджений і т. Д. Ви хочете налаштувати послідовність 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). Шеллсорт і сортувальні мережі (Видатні дисертації з комп'ютерних наук). Гарленд. ISBN 0-824-04406-1


не соромтесь запропонувати певну реалізацію :-)
kriss

Пропозиція додана. Насолоджуйтесь помилками.
gcp

3

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


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

1
Ви можете умовно додати 1 на основі cmpgtрезультату, віднімаючи його, а не маскуючи set1(1). наприклад , index = _mm256_sub_epi32(index, gt)робитьindex -= -1 or 0;
Пітер Кордес

1
eq = _mm256_insert_epi32(eq, 0, I)не є ефективним способом нульового елементу, якщо він компілюється так, як написано (особливо для елементів поза низьким 4, оскільки vpinsrdвін доступний лише з пунктом призначення XMM; індекси, що перевищують 3, повинні бути емульовані). Натомість _mm256_blend_epi32( vpblendd) з нульовим вектором. vpblenddце одноосібна інструкція, яка працює на будь-якому порту, порівняно з перетасуванням, якому потрібен порт 5 на процесорах Intel. ( agner.org/optimize ).
Пітер Кордес

1
Крім того, ви можете розглянути можливість створення rotвекторів з різними перемичками з одного джерела або принаймні запустити 2 ланцюги депулетів паралельно, які ви використовуєте по черзі, замість одного єдиного ланцюга депо через перемикання смуги проходження (3 циклу затримки). Це збільшить ILP в межах одного сорту. 2 ланцюги депіляції обмежують кількість констант вектора до розумного числа, просто 2: 1 для одного обертання і один для 2 кроків обертання разом.
Пітер Кордес

2

Це питання стає досить старим, але мені справді довелося вирішити ту саму проблему і сьогодні: швидкі агоритми для сортування невеликих масивів. Я думав, що було б гарною ідеєю поділитися своїми знаннями. Поки я вперше почав використовувати сортування мереж, мені нарешті вдалося знайти інші алгоритми, для яких загальна кількість порівнянь, проведених для сортування кожної перестановки на 6 значень, була меншою, ніж для сортування мереж, і меншою, ніж при сортуванні вставки. Я не рахував кількості свопів; Я б очікував, що він буде приблизно рівнозначним (можливо, часом трохи вище).

В алгоритмі sort6використовується алгоритм, sort4який використовує алгоритм sort3. Ось реалізація у легкій формі C ++ (оригінал є важким для шаблону, щоб він міг працювати з будь-яким ітератором випадкового доступу та будь-якою відповідною функцією порівняння).

Сортування 3 значень

Наступний алгоритм - це нерозгорнутий вид вставки. Коли потрібно здійснити два заміни (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 призначення для сортування трьох значень.

Сортування 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 свопів. Розгортати сортування вставки легко, але ми будемо використовувати інший алгоритм для останнього сортування ...

Сортування 6 значень

Ця версія використовує розкручену версію того, що я назвав сортом подвійної вставки . Назва не така велика, але вона досить описова, ось як це працює:

  • Сортувати все, окрім першого та останнього елементів масиву.
  • Поміняйте місцями перший і елементи масиву, якщо перший більший за останній.
  • Вставте перший елемент у відсортовану послідовність спереду, потім останній елемент іззаду.

Після заміни перший елемент завжди менший, ніж останній, а це означає, що при вставлянні їх у відсортовану послідовність не буде більше N порівнянь, щоб вставити два елементи в гіршому випадку: наприклад, якщо перший елемент був вставлений у 3-му положенні, потім останній не можна вставити нижче 4-го положення.

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 в гіршому випадку.

Я сподіваюся, що це допоможе, навіть якщо це питання вже не представляє актуальної проблеми :)

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


Ці приємні. Оскільки проблема, яка вирішується, вже багато десятиліть, ймовірно, як і старе програмування на С, то питання, яке зараз має майже 5 років, виглядає не так актуально.
kriss

Ви повинні подивитися на те, як приурочені інші відповіді. Справа в тому, що при таких невеликих порівняннях підрахунку наборів даних, або навіть порівняннях та свопінгах насправді не сказано, наскільки швидкий алгоритм (в основному сортування 6 входів - це завжди O (1), оскільки O (6 * 6) - O (1)). Поточний найшвидший із запропонованих раніше рішень - це негайно знаходження позиції кожного значення за допомогою великого порівняння (за RexKerr).
kriss

@kriss Це зараз найшвидше? З мого читання результатів підхід до сортування мереж був найшвидшим, моє погано. Це також правда, що моє рішення походить з моєї загальної бібліотеки і що я не завжди порівнюю цілі числа, ні завжди використовуюoperator< для порівняння. Крім об'єктивного підрахунку порівнянь та свопів, я також належним чином присвоїв свої алгоритми; це рішення було найшвидшим загальним, але я дійсно пропустив @ RexKerr.
Зробимо

Рішення RexKerr (Order Rank) стало найшвидшим у архітектурі X86, оскільки gcc-компілятор 4.2.3 (а на gcc 4.9 став майже в два рази швидшим, ніж другий найкращий). Але це сильно залежить від оптимізацій компілятора і може не бути правдою для інших архітектур.
kriss

@kriss Це цікаво знати. І я міг би дійсно більше розбіжностей знову-O3 . Я думаю, що тоді я прийму іншу стратегію для моєї бібліотеки сортування: надання трьох видів алгоритмів для низької кількості порівнянь, низької кількості замінів або потенційно найкращої продуктивності. Принаймні, те, що станеться, буде для читача прозорим. Дякую за розуміння :)
Morwenn

1

Я вважаю, що у вашому питанні є дві частини.

  • Перший полягає у визначенні оптимального алгоритму. Це робиться - принаймні в цьому випадку - шляхом прокручування всіх можливих замовлень (їх не так багато), що дозволяє обчислити точний хв, макс, середнє та стандартне відхилення порівнянь та свопів. Майте також підготівку або два підручних.
  • Друга - оптимізація алгоритму. Багато можна зробити, щоб перетворити приклади коду підручника в середні та скорочувати алгоритми реального життя. Якщо ви усвідомлюєте, що алгоритм не може бути оптимізований в необхідній мірі, спробуйте підготувати.

Я б не переживав надто про спорожнення трубопроводів (якщо припустити поточний x86): передбачення гілок пройшло довгий шлях. Я б турбувався про те, щоб переконатися, що код і дані вміщуються в одному рядку кешу кожен (можливо, два для коду). Потрапивши до запізнень на отримання оновлень, це компенсує будь-яку стійку. Це також означає, що ваш внутрішній цикл буде, можливо, десятьма інструкціями або близько того, де саме воно має бути (у моєму алгоритмі сортування є дві різні внутрішні петлі, вони відповідно 10 інструкцій / 22 байти та 9/22). Якщо припустимо, що код не містить дівок, ви можете бути впевнені, що він буде сліпуче швидким.


Я не впевнений, як зрозуміти вашу відповідь. По-перше, я зовсім не розумію, який алгоритм ви пропонуєте? І як це може бути оптимальним, якщо вам доведеться пройти цикл через 720 можливих замовлень (існуючі відповіді займають набагато менше 720 циклів). Якщо у вас є випадкові дані, я не можу уявити (навіть на теоретичному рівні), як прогнозування галузей могло б працювати краще, ніж 50-50, за винятком випадків, коли це взагалі не хвилює вхідних даних. Крім того, більшість запропонованих хороших рішень вже можуть повністю працювати з даними та кодом у кеші. Але, можливо, я цілком неправильно зрозумів вашу відповідь. Розум показує якийсь код?
kriss

Я мав на увазі те, що є лише 720 (6!) Різних комбінацій з 6 цілих чисел, і, запустивши їх усі за допомогою алгоритмів-кандидатів, ви можете визначити багато речей, як я вже згадував - це теоретична частина. Практична частина - це точна настройка алгоритму для виконання якомога менше тактових циклів. Мій вихідний пункт для сортування 6 цілих чисел - оболонка з розривом 1, 4. 4 прогалини відкривають шлях для гарного прогнозування галузей у проміжку 1.
Олоф Форшелл

Шкаралупа 1, 4 розриву на 6! унікальні комбінації (починаючи з 012345 і закінчуючи 543210) матимуть найкращий випадок із 7 порівнянь та 0 обмінів та найгіршим із 14 порівнянь та 10 обмінів. Середній випадок - це близько 11,14 порівнянь та 6 обмінів.
Олоф Форшелл

1
Я не отримую "регулярного випадкового розподілу" - те, що я роблю, - це тестування кожної можливої ​​комбінації та визначення мінімальної / середньої / максимальної статистики. Shellsort - це серія різновидів вставок із зменшенням приросту таким чином, що кінцевий приріст - 1 - робить набагато менше роботи, ніж якщо він виконується самостійно, як у чистому вигляді вставки. Що стосується підрахунку тактових годин, мій алгоритм вимагає в середньому 406 тактових циклів, і це включає збір статистики та здійснення двох дзвінків до фактичної процедури сортування - по одному для кожного проміжку. Це на мобільному телефоні Athlon M300, компіляторі OpenWatcom.
Олоф Форшелл

1
"регулярний випадковий розподіл" означає, що всі комбінації фактичних даних, відсортованих, можуть не мати однакової ймовірності. Якщо всі комбінації не мають однакової ймовірності, ваша статистика порушена, тому що середня потреба повинна враховувати, скільки разів може відбутися даний розподіл. Для підрахунку годин, якщо ви спробуєте будь-яку іншу подібну реалізацію (наведені вище посилання) та запустіть її у вашій тестовій системі, ми створимо основу для порівняння та побачимо, наскільки добре виконаний ваш обраний.
kriss

1

Я знаю, це старе питання.

Але я просто написав інше рішення, яким хочу поділитися.
Не використовуючи нічого, крім вкладеного MIN MAX,

Це не швидко, оскільки він використовує 114 з кожного,
може зменшити його до 75 досить просто -> pastebin

Але тоді це вже не мінімально максимум.

Що може працювати, це робити min / max для декількох цілих чисел одночасно з AVX

Посилання на PMINSW

#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;
}

1
завжди приємно бачити нові рішення. Схоже, можлива проста оптимізація. Зрештою, він може виявитися не таким відмінним від сортування мереж.
kriss

Так, кількість MIN і MAX можливо може бути зменшено, наприклад, MIN (AB, CD) повторюється кілька разів, але зменшити їх багато буде важко, я думаю. Я додав ваші тестові випадки.
PrincePolka

pmin / maxsw працюють на упакованих 16-бітових цілих числах ( int16_t). Але ваша функція C стверджує, що вона сортує масив int(який є 32-розрядним у всіх реалізаціях C, які підтримують цей asmсинтаксис). Ви перевірили це лише малими додатними цілими числами, у яких у високих половинах лише 0? Це спрацює ... Для intвас потрібен SSE4.1 pmin/maxsd(d = dword). felixcloutier.com/x86/pminsd:pminsq або pminusdдля uint32_t.
Пітер Кордес

1

Я виявив, що принаймні в моїй системі функції 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 ++ певні гарантії про те, що може і не може статися в пам'яті , що ітератор посилається, що він в іншому випадку не було б , і саме ці гарантії , які дозволяють г ++ в краще оптимізуйте код сортування (наприклад, за допомогою покажчиків, компілятор не може бути впевнений, що всі покажчики вказують на різні місця пам'яті). Якщо я правильно пам’ятаю, це також є частиною причини, чому так багато алгоритмів STL, як-отstd::sort() , , мають такі нецензурно хороші показники.

Крім того, sort6_iterator()в деякі раз (знову ж , в залежності від контексту , в якому функція називається) послідовно поступаються функції наступної сортування, яка копіює дані в локальні змінні до їх сортування. 1 Зауважте, що оскільки визначено лише 6 локальних змінних, якщо ці локальні змінні є примітивними, вони, ймовірно, ніколи фактично не зберігаються в оперативній пам’яті, а натомість лише зберігаються в регістрах процесора до кінця виклику функції, що допомагає зробити це сортування функціонувати швидко. (Це також допомагає компілятору знати, що окремі локальні змінні мають чіткі місця в пам'яті).

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 незмінно добре оптимізується, незалежно від того, в якому контексті виклик функції сортування відображається через 1 , залежно від того, як ви передаєте вхід, спробуйте один із наступних двох алгоритми:

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
}

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

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, але не з функціями шаблону).

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

0

Спробуйте сортувати "об’єднати відсортований список". :) Використовуйте два масиви. Найшвидший для малого та великого масиву.
Якщо ви думаєте, ви тільки перевіряєте, де вставити. Інші великі значення вам не потрібно порівняти (cmp = ab> 0).
Для 4 чисел ви можете використовувати систему 4-5 cmp (~ 4.6) або 3-6 cmp (~ 4.9). Сорт бульбашки використовує 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-код

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;
	}
}


0

Сортуйте 4 елементи за допомогою cmp == 0. Кількість cmp становить ~ 4,34 (у FF натиснено ~ 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;
}

1
Випадок використання дещо відрізняється від початкового контексту питання. При фіксованій довжині сортування деталей має значення, а підрахунку cmp свопів недостатньо. Я б навіть не здивувався, якби це був не власне такий вид, який би вимагав часу, а щось зовсім інше легке виклик typeof () в init. Я не знаю, як виконати фактичну обробку часу за допомогою Javascript. Може, з вузлом?
крис

0

Може бути , я буду пізно до партії, але , по крайней мере , мій внесок є новим підходом.

  • Код дійсно повинен бути вписаний
  • навіть якщо вони накреслені, є занадто багато гілок
  • частина, що аналізується, в основному O (N (N-1)), що здається нормальним для N = 6
  • код може бути більш ефективним, якби вартістьswap була б вищою (irt the value of compare)
  • Я довіряю накресленим статичним функціям.
  • Метод пов'язаний з ранговим сортом
    • замість рангів використовуються відносні ранги (компенсації).
    • сума рангів дорівнює нулю для кожного циклу в будь-якій групі перестановки.
    • замість того, SWAP()щоб використовувати два елементи, цикли переслідуються, потребуючи лише однієї темп, а один (регістр-> реєстр) своп (новий <- старий).

Оновлення: трохи змінив код, деякі люди використовують компілятори C ++ для компіляції коду 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);
}

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

@kriss en.wikipedia.org/wiki/Permutation_group Це, безумовно, не сортування бульбашок: код виявляє цикли в заданій перестановці і здійснює ці цикли, ставлячи кожен елемент на остаточне місце. Заключна wsort6()функція має правильний інтерфейс.
joop

@joop: мій поганий, ніякий міхур не справді. Це було сказано в контексті, я все ще очікую, що код буде набагато гіршим, ніж будь-яка інша реалізація. До речі, рішення Rank Order є оптимальним щодо кількості свопів, оскільки воно безпосередньо знаходить остаточне положення кожного товару. Також незрозуміло, чи працює навіть Walksort, коли ми видаляємо гіпотезу про те, що всі сортовані числа різні, як тут. Для порівняння коду нам слід відстежити код. Крім того, як я зазвичай компілюю компілятор C ++, код не працюватиме, оскільки ОП назвав змінну "new" (і це порушує підсвічування синтаксису).
kriss

Метод дуже близький до рангового порядку, на місці виконуються лише остаточні завдання . Крім рангів o1..o5, немає потреби у другому e[6]масиві темпів . І: компілювання коду С на компілятор C ++ і звинувачення в коді?
joop

@greybeard: дякую, я додав пробіл раніше #include. Виправлено
wildplasser

0
//Bruteforce compute unrolled count dumbsort(min to 0-index)
void bcudc_sort6(int* a)
{
    int t[6] = {0};
    int r1,r2;

    r1=0;
    r1 += (a[0] > a[1]);
    r1 += (a[0] > a[2]);
    r1 += (a[0] > a[3]);
    r1 += (a[0] > a[4]);
    r1 += (a[0] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[0];

    r2=0;
    r2 += (a[1] > a[0]);
    r2 += (a[1] > a[2]);
    r2 += (a[1] > a[3]);
    r2 += (a[1] > a[4]);
    r2 += (a[1] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[1];

    r1=0;
    r1 += (a[2] > a[0]);
    r1 += (a[2] > a[1]);
    r1 += (a[2] > a[3]);
    r1 += (a[2] > a[4]);
    r1 += (a[2] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[2];

    r2=0;
    r2 += (a[3] > a[0]);
    r2 += (a[3] > a[1]);
    r2 += (a[3] > a[2]);
    r2 += (a[3] > a[4]);
    r2 += (a[3] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[3];

    r1=0;
    r1 += (a[4] > a[0]);
    r1 += (a[4] > a[1]);
    r1 += (a[4] > a[2]);
    r1 += (a[4] > a[3]);
    r1 += (a[4] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[4];

    r2=0;
    r2 += (a[5] > a[0]);
    r2 += (a[5] > a[1]);
    r2 += (a[5] > a[2]);
    r2 += (a[5] > a[3]);
    r2 += (a[5] > a[4]);
    while(t[r2]){r2++;} 
    t[r2] = a[5];

    a[0]=t[0];
    a[1]=t[1];
    a[2]=t[2];
    a[3]=t[3];
    a[4]=t[4];
    a[5]=t[5];
}

static __inline__ void sort6(int* a)
{
    #define wire(x,y); t = a[x] ^ a[y] ^ ( (a[x] ^ a[y]) & -(a[x] < a[y]) ); a[x] = a[x] ^ t; a[y] = a[y] ^ t;
    register int t;

    wire( 0, 1); wire( 2, 3); wire( 4, 5);
    wire( 3, 5); wire( 0, 2); wire( 1, 4);
    wire( 4, 5); wire( 2, 3); wire( 0, 1); 
    wire( 3, 4); wire( 1, 2); 
    wire( 2, 3);

    #undef wire
}

Незалежно від швидкості ви впевнені, що вона працює? У грубому сортуванні ваші петлі сумнівні. Мені здається, вони не спрацюють, якщо у нас буде нуль у відсортованих значеннях.
kriss

1
t [6] масив ініціалізується до 0x0. Тож не має значення, де і якщо буде записаний ключ із значенням 0x0.
ФранГ

-1

Добре, якщо це лише 6 елементів, і ви можете використовувати паралелізм, хочете звести до мінімуму умовне розгалуження і т. Д. Чому ви не створюєте всі комбінації та перевіряєте на замовлення? Я б задумався, що в деяких архітектурах це може бути досить швидко (доки пам'ять не буде виділена)


9
Замовлень 720, а швидкі версії знаходяться за 100 циклів. Навіть якщо масштабний паралелізм міг би бути важелем, за такий невеликий часовий масштаб вартість створення та синхронізації потоків, ймовірно, перевищить витрати на просто сортування масивів на одному ядрі.
Кевін Сток

-3

Ось три типові методи сортування, які представляють три різні класи сортування алгоритмів:

Insertion Sort: Θ(n^2)

Heap Sort: Θ(n log n)

Count Sort: Θ(3n)

Але перегляньте дискусію Стефана Нельссона щодо найшвидшого алгоритму сортування? де він обговорює рішення, яке зводиться до O(n log log n).. перевірити його реалізацію в С

Цей алгоритм напівлінійного сортування був представлений документом у 1995 році:

А. Андерссон, Т. Хагерпуп, С. Нільссон та Р. Раман. Сортування за лінійним часом? У матеріалах 27-го щорічного симпозіуму АСМ з теорії обчислень, стор. 427-436, 1995.


8
Це цікаво, але поруч. Big-Θ призначений для приховування постійних факторів і показує тенденцію, коли розмір проблеми (n) збільшується. Тут проблема полягає повністю у фіксованому розмірі проблеми (n = 6) та врахуванні постійних факторів.
kriss

@kriss Ви маєте рацію, моє порівняння є асимптотичним, тому практичне порівняння покаже, чи швидше це чи не для цього випадку
Khaled.K

4
Ви не можете зробити висновок, оскільки кожен різний алгоритм приховує різну K мультиплікативну константу (а також константну добавку C). тобто: k0, c0 для сортування вставки, k1, c1 для сортування купи тощо. Усі ці постійні фактично різні (ви можете сказати у фізичних термінах, що у кожного алгоритму є свій власний "коефіцієнт тертя"), ви не можете зробити висновок, що алгоритм дійсно швидший у цьому випадку (або будь-який фіксований n випадок).
kriss
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.