OpenMP 通用 qsort 加速不足
OpenMP generic qsort lackluster speedup
因此,我的实现似乎在大约 10 亿个元素时与基本串行 qsort 收支平衡。大多数在线并行 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;
}
您创建的每个 OMP 任务都会产生一些开销,并且您的特定任务会变得越来越小。随着每个任务的工作量变小,开销也会成比例地增加。串行快速排序的一些常见优化技术可能不仅对基本算法有帮助,而且对您的开销问题也有帮助。
通过切换小子数组的策略,您可以大大减少所涉及的任务总数,从而减少相关的开销。这与针对小子数组切换到插入排序的常见快速排序优化非常吻合。 "small" 的定义是一个可调参数,其最佳值在某种程度上取决于您要排序的内容,但 5 - 30 范围内的值可能对您来说是一个很好的截止值。当你进行这样的切换时,在一个任务中执行整个子数组插入排序。
您也可能受益于仅对每对子数组中较小的子数组进行递归,而不是循环处理较大的子数组。这将最大递归深度限制为 O(log n),否则在最坏情况下为 O(n)。由于每个递归涉及其自己的任务,这也将减少至少两倍所需的任务总数。
选择好的枢轴是快速排序性能的核心问题之一,但枢轴选择算法的相对效果高度依赖于数据。我建议至少 little 比总是选择最左边的元素更复杂——在一般情况下,三中位数或随机枢轴选择可能会产生更好的性能.由于枢轴的选择会影响子数组大小,这与创建的任务数量和它们的大小有关,这可能是您的并行版本的额外胜利。
因此,我的实现似乎在大约 10 亿个元素时与基本串行 qsort 收支平衡。大多数在线并行 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;
}
您创建的每个 OMP 任务都会产生一些开销,并且您的特定任务会变得越来越小。随着每个任务的工作量变小,开销也会成比例地增加。串行快速排序的一些常见优化技术可能不仅对基本算法有帮助,而且对您的开销问题也有帮助。
通过切换小子数组的策略,您可以大大减少所涉及的任务总数,从而减少相关的开销。这与针对小子数组切换到插入排序的常见快速排序优化非常吻合。 "small" 的定义是一个可调参数,其最佳值在某种程度上取决于您要排序的内容,但 5 - 30 范围内的值可能对您来说是一个很好的截止值。当你进行这样的切换时,在一个任务中执行整个子数组插入排序。
您也可能受益于仅对每对子数组中较小的子数组进行递归,而不是循环处理较大的子数组。这将最大递归深度限制为 O(log n),否则在最坏情况下为 O(n)。由于每个递归涉及其自己的任务,这也将减少至少两倍所需的任务总数。
选择好的枢轴是快速排序性能的核心问题之一,但枢轴选择算法的相对效果高度依赖于数据。我建议至少 little 比总是选择最左边的元素更复杂——在一般情况下,三中位数或随机枢轴选择可能会产生更好的性能.由于枢轴的选择会影响子数组大小,这与创建的任务数量和它们的大小有关,这可能是您的并行版本的额外胜利。