OpenMP generic 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 задача, която създавате, а конкретните ви задачи стават все по-малки. Тъй като работата за задача става малка, режийните разходи стават пропорционално по-скъпи. Някои от обичайните техники за оптимизация за сериен QuickSort могат да помогнат не само с основния алгоритъм, но и с вашия проблем с режийните разходи.

Бихте могли значително да намалите общия брой включени задачи и следователно свързаните с тях допълнителни разходи, като смените стратегията за малки подмасиви. Това ще се съчетае добре с общата оптимизация на Quicksort при преминаване към сортиране чрез вмъкване за малки подмасиви. Дефиницията на „малък“ е регулируем параметър и неговата оптимална стойност зависи донякъде от това какво сортирате, но вероятно нещо в диапазона от 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 call в swapelement, той спадна до 70-80 секунди! Мисля, че имах затруднения с твърде много обаждания за разпределяне на стека, така че превключването към стека помогна много! - person Nate Dellinger; 15.05.2015