Самый быстрый вид массива с фиксированной длиной 6 int - PullRequest
387 голосов
/ 07 мая 2010

Отвечая на другой вопрос переполнения стека ( этот ), я наткнулся на интересную подзадачу. Какой самый быстрый способ сортировки массива из 6 целых чисел?

Поскольку вопрос очень низкого уровня:

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

На самом деле этот вопрос - своего рода гольф, цель которого не в том, чтобы минимизировать длину источника, а во время выполнения. Я называю его «кодом Зенинга», который используется в названии книги Оптимизация Дзен кода от Майкл Абраш и продолжения .

Что интересно, есть несколько слоев:

  • пример прост и легок для понимания и измерения, не требующий много навыков C
  • показывает эффекты выбора хорошего алгоритма для этой проблемы, а также эффекты компилятора и базового оборудования.

Вот моя эталонная (наивная, не оптимизированная) реализация и мой набор тестов.

#include <stdio.h>

static __inline__ int sort6(int * d){

    char j, i, imin;
    int tmp;
    for (j = 0 ; j < 5 ; j++){
        imin = j;
        for (i = j + 1; i < 6 ; i++){
            if (d[i] < d[imin]){
                imin = i;
            }
        }
        tmp = d[j];
        d[j] = d[imin];
        d[imin] = tmp;
    }
}

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int main(int argc, char ** argv){
    int i;
    int d[6][5] = {
        {1, 2, 3, 4, 5, 6},
        {6, 5, 4, 3, 2, 1},
        {100, 2, 300, 4, 500, 6},
        {100, 2, 3, 4, 500, 6},
        {1, 200, 3, 4, 5, 600},
        {1, 1, 2, 1, 2, 1}
    };

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6 ; i++){
        sort6(d[i]);
        /*
         * printf("d%d : %d %d %d %d %d %d\n", i,
         *  d[i][0], d[i][6], d[i][7],
         *  d[i][8], d[i][9], d[i][10]);
        */
    }
    cycles = rdtsc() - cycles;
    printf("Time is %d\n", (unsigned)cycles);
}

Необработанные результаты

Поскольку число вариантов становится большим, я собрал их все в наборе тестов, который можно найти здесь . Благодаря Кевину Сток, используемые тесты немного менее наивны, чем показанные выше. Вы можете скомпилировать и выполнить его в своей среде. Мне весьма интересно поведение на разных целевых архитектурах / компиляторах. (Хорошо, ребята, вставьте это в ответы, я буду +1 каждому вкладчику нового набора результатов).

Я дал ответ Даниэлю Штутцбаху (для игры в гольф) год назад, когда он был источником самого быстрого решения в то время (сортировка сетей).

Linux 64 бит, gcc 4.6.1 64 бит, Intel Core 2 Duo E8400, -O2

  • Прямой вызов функции библиотеки qsort: 689,38
  • Наивная реализация (вставка сортировки): 285,70
  • Сортировка вставок (Даниэль Штутцбах): 142.12
  • Сортировка вставок развернута: 125,47
  • Порядок ранга: 102.26
  • Порядок ранга с регистрами: 58.03
  • Сортировка сетей (Даниэль Штутцбах): 111,68
  • Сортировка сетей (Paul R): 66,36
  • Сортировка сетей 12 с быстрой заменой: 58,86
  • Сортировка сетей 12 переупорядочено Своп: 53,74
  • Сортировка сетей 12 переупорядочено Простой обмен: 31,54
  • Переупорядоченная сеть сортировки с быстрой заменой: 31,54
  • Переупорядоченная сеть сортировки с быстрой заменой V2: 33,63
  • Сортировка пузырьков (Паоло Бонзини): 48,85
  • Развернутая сортировка вставок (Паоло Бонзини): 75,30

Linux 64 бит, gcc 4.6.1 64 бит, Intel Core 2 Duo E8400, -O1

  • Прямой вызов функции библиотеки qsort: 705,93
  • Наивная реализация (вставка сортировки): 135.60
  • Сортировка вставок (Даниэль Штутцбах): 142,11
  • Сортировка вставок развернута: 126,75
  • Порядок ранга: 46.42
  • Порядок ранга с регистрами: 43,58
  • Сортировка сетей (Даниэль Штутцбах): 115,57
  • Сортировка сетей (Paul R): 64,44
  • Сортировка сетей 12 с быстрой заменой: 61,98
  • Сортировка сетей 12 переупорядочено Своп: 54,67
  • Сортировка сетей 12 переупорядочено Простой обмен: 31,54
  • Переупорядоченная сеть сортировки с быстрой заменой: 31,24
  • Переупорядоченная сеть сортировки с быстрой заменой V2: 33,07
  • Сортировка пузырьков (Паоло Бонзини): 45,79
  • Развернутая сортировка вставок (Паоло Бонзини): 80,15

Я включил результаты как -O1, так и -O2, потому что неожиданно для нескольких программ O2 * меньше эффективнее, чем O1. Интересно какая конкретно оптимизация имеет этот эффект?

Комментарии к предлагаемым решениям

Сортировка вставок (Даниэль Штутцбах)

Как и ожидалось, минимизация ветвей - действительно хорошая идея.

Сортировка сетей (Даниэль Штутцбах)

Лучше, чем сортировка вставок. Я задавался вопросом, не был ли главный эффект от избегания внешнего цикла. Я проверил развернутой сортировкой вставки, чтобы проверить, и мы действительно получаем примерно одинаковые цифры (код здесь ).

Сортировка сетей (Paul R)

Лучшее на данный момент. Фактический код, который я использовал для проверки: здесь . Пока не знаю, почему это почти в два раза быстрее, чем в другой сети сортировки. Передача параметров? Быстро макс?

Сортировка сетей 12 SWAP с быстрой заменой

По предложению Даниэля Штутцбаха, я объединил его сеть сортировки с 12 свопами и быстрый обмен без ответвлений (код здесь ). Это действительно быстрее, лучше всего с небольшим запасом (примерно 5%), как и следовало ожидать, используя 1 своп меньше.

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

Вызов библиотеки qsort

Чтобы дать еще одну контрольную точку, я также попытался, как предлагалось, просто вызвать библиотеку qsort (код здесь ). Как и ожидалось, он намного медленнее: в 10-30 раз медленнее ... как стало очевидно с новым набором тестов, основная проблема, похоже, заключается в начальной загрузке библиотеки после первого вызова, и она не так плохо сравнивается с версия. Это только в 3-20 раз медленнее в моем Linux. В некоторых архитектурах, используемых для тестирования другими, это кажется даже быстрее (я очень удивлен тем, что библиотека qsort использует более сложный API).

Порядок ранга

Рекс Керр предложил другой совершенно другой метод: для каждого элемента массива вычисляют непосредственно его конечную позицию. Это эффективно, потому что порядок вычисления не требует ветвления. Недостаток этого метода состоит в том, что он занимает в три раза больше памяти массива (одна копия массива и переменные для хранения ранговых порядков). Результаты производительности очень удивительны (и интересны). На моей эталонной архитектуре с 32-битной ОС и Intel Core2 Quad E8300 число циклов было чуть ниже 1000 (как в случае сортировки сетей с разветвленной ветвью). Но когда он был скомпилирован и выполнен на моем 64-битном компьютере (Intel Core2 Duo), он работал намного лучше: он стал самым быстрым до сих пор. Я наконец выяснил истинную причину. Мой 32-битный блок использует gcc 4.4.1 и мой 64-битный блок gcc 4.4.3, и последний, похоже, гораздо лучше оптимизирует этот конкретный код (для других предложений было очень мало различий).

обновление

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

Сортировка сетей 12 с переупорядоченным свопом

Удивительная эффективность предложения Rex Kerr с gcc 4.4.3 заставила меня задуматься: как может программа с 3-кратным увеличением использования памяти быть быстрее, чем сети с разветвленной сортировкой?Моя гипотеза состояла в том, что у него было меньше зависимостей типа чтения после записи, что позволяло лучше использовать планировщик суперскалярных команд в x86.Это дало мне идею: изменить порядок перестановок, чтобы минимизировать зависимости чтения после записи.Проще говоря: когда вы делаете SWAP(1, 2); SWAP(0, 2);, вам нужно дождаться завершения первого обмена, прежде чем выполнять второй, потому что оба имеют доступ к общей ячейке памяти.Когда вы делаете SWAP(1, 2); SWAP(4, 5);, процессор может выполнять оба параллельно.Я попробовал, и все работает как положено, сортировочные сети работают примерно на 10% быстрее.

Сортировка сетей 12 с помощью простого свопа

Через год после первоначальной публикации Стейнар Х. Гандерсон предложил, чтобы мы не пытались перехитрить компилятор и сохранить код подкачкипросто.Это действительно хорошая идея, поскольку полученный код работает примерно на 40% быстрее!Он также предложил обмен, оптимизированный вручную с использованием встроенного кода сборки x86, который может сэкономить еще несколько циклов.Самое удивительное (в нем говорится о психологии программиста), что год назад никто из бывших в употреблении не пробовал эту версию свопинга.Код, который я использовал для проверки: здесь .Другие предлагали другие способы написания быстрого свопинга на C, но он дает те же характеристики, что и простой с достойным компилятором.

«Лучший» код теперь выглядит следующим образом:

static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x) 
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
                    const int b = max(d[x], d[y]); \
                    d[x] = a; d[y] = b; }
    SWAP(1, 2);
    SWAP(4, 5);
    SWAP(0, 2);
    SWAP(3, 5);
    SWAP(0, 1);
    SWAP(3, 4);
    SWAP(1, 4);
    SWAP(0, 3);
    SWAP(2, 5);
    SWAP(1, 3);
    SWAP(2, 4);
    SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}

Если мы считаем, что наш набор тестов (и, да, он довольно плохой, просто преимущество в том, что он короткий, простой и легкий для понимания того, что мы измеряем), среднее число циклов полученного кода для одного вида будет ниже 40 циклов.(Выполнено 6 тестов).Это означает, что каждый своп в среднем составляет 4 цикла.Я называю это удивительно быстро.Возможны ли другие улучшения?

Ответы [ 24 ]

5 голосов
/ 25 августа 2011

С нетерпением жду возможности попробовать свои силы в этом и учиться на этих примерах, но сначала несколько моментов из моей 1,5 ГГц PPC Powerbook G4 с 1 ГБ оперативной памяти DDR.(Я заимствовал аналогичный rdtsc-подобный таймер для PPC от http://www.mcs.anl.gov/~kazutomo/rdtsc.html для таймингов.) Я запускал программу несколько раз, и абсолютные результаты менялись, но самый быстрый тест был «Insertion Sort (Daniel Stutzbach)»,с "Insertion Sort Unrolled" близкой секундой.

Вот последний набор времени:

**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)**     : 85
**Insertion Sort Unrolled**               : 97
**Sorting Networks (Daniel Stutzbach)**   : 457
**Sorting Networks (Paul R)**             : 179
**Sorting Networks 12 with Fast Swap**    : 238
**Sorting Networks 12 reordered Swap**    : 236
**Rank Order**                            : 116
4 голосов
/ 14 марта 2012

Вот мой вклад в эту тему: оптимизированная 1,4-разрядная сортировка оболочки для вектора int из 6 элементов (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 ГГц (память DDR2) он работает за 165 тактов. Это среднее значение, рассчитанное по времени для каждой уникальной последовательности (всего 6! / 720). Скомпилировано в Win32 с использованием OpenWatcom 1.8. Цикл по сути является сортировкой вставки и имеет длину 16 инструкций / 37 байт.

У меня нет 64-битной среды для компиляции.

3 голосов
/ 05 сентября 2016

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

3 голосов
/ 24 августа 2011

Если сортировка вставок здесь достаточно конкурентоспособна, я бы рекомендовал попробовать сортировку оболочек.Боюсь, что 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 элементов, кто знает ...

Согласно Википедии, это может даже сочетаться с сортировкойсети: Пратт, В. (1979).Оболочки сортировочные и сортировочные сети (Выдающиеся диссертации в области компьютерных наук).Garland.ISBN 0-824-04406-1

2 голосов
/ 07 октября 2015

Этот вопрос становится довольно старым, но на самом деле мне пришлось решать ту же проблему в наши дни: быстрые агорифмы для сортировки небольших массивов. Я подумал, что будет хорошей идеей поделиться своими знаниями. Хотя я впервые начал использовать сортировку сетей, мне, наконец, удалось найти другие алгоритмы, для которых общее число сравнений, выполненных для сортировки каждой перестановки из 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 в худшем случае.

Я надеюсь, что это поможет, даже если этот вопрос больше не представляет реальной проблемы:)

РЕДАКТИРОВАТЬ: после помещения его в предоставленный тест, он очищается медленнее, чем большинство интересных альтернатив. Это имеет тенденцию работать немного лучше, чем развернутая сортировка вставок, но это в значительной степени так. По сути, это не лучшая сортировка для целых чисел, но она может быть интересна для типов с дорогой операцией сравнения.

1 голос
/ 06 марта 2012

Я полагаю, что на ваш вопрос есть две части.

  • Первая - определить оптимальный алгоритм.Это делается - по крайней мере, в этом случае - путем обхода каждого возможного порядка (их не так много), который позволяет вычислить точное минимальное, максимальное, среднее и стандартное отклонение сравнений и свопов.Иметь второе место или два под рукой.
  • Во-вторых, оптимизировать алгоритм.Многое можно сделать, чтобы преобразовать примеры кода из учебника в средние и простые алгоритмы.Если вы понимаете, что алгоритм не может быть оптимизирован до требуемой степени, попробуйте получить второе место.

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

1 голос
/ 28 сентября 2017

Я знаю, что это старый вопрос.

Но я только что написал другое решение, которым хочу поделиться.
Не используя ничего, кроме вложенного 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 голос
/ 03 июня 2017

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

#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)

template<class IterType> 
inline void sort6_iterator(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
  const auto b = MAX(*(it + x), *(it + y)); \
  *(it + x) = a; *(it + y) = b; }

  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

Я передал эту функцию итератору std::vector в моем временном коде. Я подозреваю, что использование итераторов дает g ++ определенные гарантии относительно того, что может и не может произойти с памятью, на которую ссылается итератор, чего в противном случае у него не было бы, и именно эти гарантии позволяют g ++ лучше оптимизировать код сортировки (который, если Я правильно помню, это также часть причины, по которой многие std алгоритмы, такие как std::sort(), обычно имеют такую ​​неприлично хорошую производительность). Однако во время синхронизации я заметил, что контекст (то есть окружающий код), в котором был сделан вызов функции сортировки, оказал значительное влияние на производительность, что, вероятно, связано с тем, что функция встроена и затем оптимизирована. Например, если программа была достаточно простой, то, как правило, не было большой разницы в производительности между передачей функции сортировки указатель и передачей ей итератора; в противном случае использование итераторов обычно приводило к заметно лучшей производительности и никогда (по моему опыту, по крайней мере, пока) никакой заметно худшей производительности. Я подозреваю, что это может быть потому, что g ++ может глобально оптимизировать достаточно простой код.

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

template<class IterType> 
inline void sort6_iterator_local(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  const auto b = MAX(data##x, data##y); \
  data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a)   auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;

  DD2(1,2)    SWAP(1, 2)
  DD2(4,5)    SWAP(4, 5)
  DD1(0)      SWAP(0, 2)
  DD1(3)      SWAP(3, 5)
  SWAP(0, 1)  SWAP(3, 4)
  SWAP(1, 4)  SWAP(0, 3)   CB(0)
  SWAP(2, 5)  CB(5)
  SWAP(1, 3)  CB(1)
  SWAP(2, 4)  CB(4)
  SWAP(2, 3)  CB(2)        CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}

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

#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  data##y = MAX(data##x, data##y); \
  data##x = a; }

Если вам просто нужен алгоритм сортировки, который gcc -O3 всегда хорошо оптимизирует, то, в зависимости от того, как вы передаете ввод, попробуйте один из следующих двух алгоритмов:

template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Или (в первых 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, чем функции vanilla C (это связано с тем, что gcc приходится иметь дело с указателями функций для функций vanilla C, но не с шаблоном функции).

0 голосов
/ 20 февраля 2019
//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
}
0 голосов
/ 01 февраля 2018

Возможно, я опаздываю на вечеринку, но по крайней мере мой вклад - новый подход.

  • Код действительно должен быть встроен
  • даже если встроено, слишком много ветвей
  • анализирующей частью в основном является O (N (N-1)), что кажется нормальным для N = 6
  • код мог бы быть более эффективным, если бы стоимость swap была бы выше (или стоимость 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);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...