Общее ускорение OpenMP qsort

Таким образом, кажется, что моя реализация не работает даже с базовым последовательным qsort около 1 миллиарда элементов. Большинство параллельных алгоритмов qsort в Интернете предназначены для сортировки целочисленных массивов и прочего, но я хочу иметь возможность сортировать что-либо с помощью пользовательских компараторов, таких как встроенный qsort, потому что я хотел бы сортировать некоторые структуры, которые у меня есть. Я использую 12 потоков и могу убедиться, что они правильно порождаются, глядя сверху. Возможно, я создаю слишком много потоков и должен перестать создавать новые потоки на основе глубины рекурсии? Я знаю, что моя реализация qsort довольно проста, и очевидно, что над встроенной qsort было вложено много работы и оптимизации, но я не понимаю, почему я не получаю хорошего ускорения с распараллеливанием. Буду очень признателен за любой вклад, так как я мог бы использовать этот код во многих областях, если бы я мог сохранить его общим. Спасибо!

void test ( void* data, uint64_t startIdx, uint64_t endIdx, size_t dataSize, int (*cmp)(const void *, const void *) )
{
    #pragma omp parallel
    {
        #pragma omp single nowait
        {
            p_qsort( data, 0, MAX_INTS - 1, sizeof (testint), cmp );
        }
    }
}

void p_qsort ( void* data, uint64_t startIdx, uint64_t endIdx, size_t dataSize, int (*cmp)(const void *, const void *) )
{
    uint64_t idx = p_qsort_partition( data, startIdx, endIdx, dataSize, cmp );

    //Left array
    if ( startIdx < idx - 1 )
    {
        #pragma omp task
        p_qsort( data, startIdx, idx - 1, dataSize, cmp );
    }
    //Right array
    if ( endIdx > idx )
    {
        #pragma omp task
        p_qsort( data, idx, endIdx, dataSize, cmp );
    }
}

void swapVoidElements ( void* el1, void* el2, size_t size )
{
    if ( el1 == el2 )
        return;

    void* temp = malloc( size );

    //temp = el1
    memcpy( temp, el1, size );
    //el1 = el2
    memcpy( el1, el2, size );
    //el2 = temp
    memcpy( el2, temp, size );

    free( temp );
}

uint64_t p_qsort_partition ( void* data, uint64_t left, uint64_t right, size_t dataSize, int (*cmp)(const void *, const void *) )
{
    void* pivotP = getVoidPtr( data, left, dataSize );
    void* pivotCmp = malloc( dataSize );
    memcpy( pivotCmp, pivotP, dataSize );

    while ( left <= right )
    {
        while ( cmp( getVoidPtr( data, left, dataSize ), pivotCmp ) < 0 )
            left++;
        //while ( array[right] > pivot )
        while ( cmp( getVoidPtr( data, right, dataSize ), pivotCmp ) > 0 )
            right--;
        //Swap
        if ( left <= right )
        {
            void* leftP = getVoidPtr( data, left, dataSize );
            void* rightP = getVoidPtr( data, right, dataSize );

            swapVoidElements( leftP, rightP, dataSize );

            left++;
            right--;
        }
    }
    free( pivotCmp );

    return left;
}

void* getVoidPtr ( void* data, uint64_t idx, size_t dataSize )
{
    uint64_t idxNum = idx * dataSize;
    char* test = ((char*) data) + idxNum;

    return (void *) test;
}

person Nate Dellinger    schedule 13.05.2015    source источник


Ответы (1)


Вы несете небольшие накладные расходы для каждой создаваемой вами задачи OMP, и ваши конкретные задачи становятся все меньше и меньше. По мере того, как работа над задачей становится меньше, накладные расходы становятся пропорционально более дорогими. Некоторые из распространенных методов оптимизации последовательной быстрой сортировки могут помочь не только с базовым алгоритмом, но и с вашей проблемой накладных расходов.

Вы можете значительно сократить общее количество задействованных задач и, следовательно, связанные с ними накладные расходы, переключив стратегию для небольших подмассивов. Это прекрасно согласуется с обычной оптимизацией быстрой сортировки при переключении на сортировку вставками для небольших подмассивов. Определение «маленький» — это настраиваемый параметр, и его оптимальное значение в некоторой степени зависит от того, что вы сортируете, но, возможно, что-то в диапазоне от 5 до 30 будет для вас хорошим пределом. При таком переключении выполните сортировку вставкой всего подмассива в одной задаче.

Вы также можете извлечь выгоду из рекурсии только для меньшего из каждой пары подмассивов и вместо этого использовать цикл для обработки больших. Это ограничивает максимальную глубину рекурсии до O(log n), тогда как в противном случае она равна O(n) в худшем случае. Поскольку каждая рекурсия включает в себя свою собственную задачу, это также сократит общее количество требуемых задач как минимум в два раза.

Выбор хороших опорных точек — одна из центральных проблем производительности QuickSort, но относительные эффекты алгоритма выбора опорной точки сильно зависят от данных. Я бы предложил что-то по крайней мере немного более сложное, чем всегда выбирать крайний левый элемент — либо медиана из трех, либо случайный опорный выбор, вероятно, в среднем даст несколько лучшую производительность. Поскольку выбор точки опоры влияет на размер подмассива, что связано с количеством созданных задач и их размерами, это может быть дополнительным преимуществом для вашей параллельной версии.

person John Bollinger    schedule 13.05.2015
comment
Простой способ избежать накладных расходов для небольших сортировок — вызвать последовательную qsort, когда количество элементов ниже некоторого порога. По моему опыту, порог должен быть порядка 500 (плюс-минус несколько множителей 2) для простых типов. Совет Джона по поводу хорошего разворота очень важен. В параллельной быстрой сортировке TBB мы используем псевдомедиану 9. См. также мой блог software.intel.com/en-us/articles/ для общих советов по параллельной сортировке. - person Arch D. Robison; 14.05.2015
comment
Джон и Арч, спасибо за отличный совет. Я буду работать над сортировкой вставками и оптимизацией количества задач, которые я создаю, а также над улучшением сводного выбора. :) Есть ли что-то странное, что я делаю с пустыми указателями? Это непросто, когда вы не можете просто вызвать arr[i] = arr[x] или что-то еще, чтобы поменять местами элементы, потому что мне действительно нужно скопировать память (см. функцию swapVoidElement). Могу ли я что-нибудь улучшить с моей идеей пустот и обобщений? Ваша помощь была высоко оценена! - person Nate Dellinger; 14.05.2015
comment
@NateDellinger, ваша функция подкачки выглядит нормально. Однако, если вы готовы принять по крайней мере C99 и что отдельные сортируемые объекты невелики, вы можете рассмотреть возможность отказа от динамического управления памятью, объявив temp массивом переменной длины: unsigned char temp[size]. Это должно быть дешевле, так как оно будет размещено в стеке. В этом случае вы убираете и malloc(), и free(), но оставляете все как есть. - person John Bollinger; 14.05.2015
comment
Это именно то, что я искал! Не был уверен, как поместить пустую информацию в стек. Встроенный qsort обрабатывает 1 миллиард элементов примерно за 372 секунды. После сортировки вставками и сокращения вызовов задач моя параллельная версия сократилась примерно до 250 секунд. Затем, как ни удивительно, после того, как я избавился от вызова malloc/free в элементе подкачки, время сократилось до 70-80 секунд! Я думаю, что у меня было узкое место из-за слишком большого количества вызовов для выделения кучи, поэтому переключение на стек очень помогло! - person Nate Dellinger; 15.05.2015