Відповідаючи на ще одне запитання про переповнення стека (на це ), я натрапив на цікаву підпроблему. Який найшвидший спосіб сортувати масив із 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 цикли. Я називаю це напрочуд швидким. Можливі інші вдосконалення?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
тому, що rdtsc ставить відповідь у EDX: EAX, тоді як GCC очікує її в єдиному 64-бітному регістрі. Ви можете бачити помилку, зібравши на -O3. Також дивіться нижче мій коментар до Пола R про швидший SWAP.
CMP EAX, EBX; SBB EAX, EAX
поставить або 0, або 0xFFFFFFFF EAX
залежно від того EAX
, більший чи менший, ніж EBX
відповідно. SBB
є "відняти з позикою", аналог ADC
("додати з нести"); біт статусу, на який ви посилаєтесь, - це біт перенесення. Потім я знову пам’ятаю це ADC
і SBB
мав жахливу затримку та пропускну здатність на Pentium 4 vs. ADD
та SUB
, і все ще був удвічі повільнішим на основних процесорах. Починаючи з 80386, існують також інструкції з SETcc
умовного зберігання та CMOVcc
умовного переміщення, але вони також повільні.
x-y
іx+y
не спричиняє переливу чи переповнення?