我有以下代码设置:
#define TSIZE 32
#define TNUM 24000000
#define CORES 4
/* Byte-wise swap two items of size SIZE. */
#define SWAP(a, b, size) \
do \
{ \
size_t __size = (size); \
char *__a = (a), *__b = (b); \
do \
{ \
char __tmp = *__a; \
*__a++ = *__b; \
*__b++ = __tmp; \
} while (--__size > 0); \
} while (0)
char* TWEETS;
size_t partition(void* arr, size_t left, size_t right, int (*compar)(const void* , const void*))
{
char* cArr = (char*) arr;
size_t i;
size_t pivotIndex = (left+right)/2;
char* pivotValue = &cArr[(size_t)TSIZE * right];
size_t index = left;
SWAP(&cArr[(size_t)TSIZE * pivotIndex], &cArr[(size_t)TSIZE * right], (size_t)TSIZE);
for(i = left; i < right; i++) {
if(compar((void*) &cArr[(size_t)TSIZE * i], (void*) pivotValue) < 0) {
SWAP(&cArr[(size_t)TSIZE * i], &cArr[(size_t)TSIZE * index], (size_t)TSIZE);
index++;
}
}
SWAP(&cArr[(size_t)TSIZE * index], &cArr[(size_t)TSIZE * right], (size_t)TSIZE);
return index;
}
void quicksort(void* base, size_t left, size_t right, int (*compar)(const void* , const void*))
{
if(left < right) {
size_t pivot = partition(base, left, right, compar);
#pragma omp task
quicksort(base, left, pivot-1, compar);
#pragma omp task
quicksort(base, pivot+1, right, compar);
}
}
int main(int argc, char** argv) {
omp_set_dynamic(0);
omp_set_num_threads(CORES);
TWEETS = (char*) malloc((size_t)TNUM * (size_t)TSIZE * (size_t)CORES * (size_t)sizeof(char));
if(TWEETS == NULL) exit(1);
readData();
#pragma omp parallel
{
#pragma omp single
quicksort(TWEETS, 0, ((size_t)CORES*(size_t)TNUM)-(size_t)1, compare);
}
free(TWEETS);
}
首先,请原谅大量的(size_t)演员表,我绝望地做了这件事。
我在这做什么
我正在阅读一个包含2400万行文本的文本文件,每行包含32个字节的字符。然后根据比较函数对行进行排序,这里我省略了。我可以保证这个功能有效并且不是我麻烦的原因。它始终返回-1,0或1
我也试图并行化quicksort算法。代码行与我使用的内核数量一起增长,例如: 1核= 2400万,2核= 4800万等等。
什么在起作用
只要文件大小低于4800万行文本,工作已经在对文件进行排序,使用1到8个核心。
我的问题是什么
我的问题是,一旦我尝试使用7200万行或更多文本对文件进行排序,快速排序算法会遇到分段错误。我已经尽可能地用gdb调试了代码,而错误的代码是这一行:
SWAP(&cArr[(size_t)TSIZE * i], &cArr[(size_t)TSIZE * index], (size_t)TSIZE);
这是for循环中分区函数的交换调用。我还可以看到,此时,变量“right”的值为18446744073709551615(2 ^ 64-1),这是分段错误的原因。 “右”的最大值应该是TSIZE * TNUM * CORES。由于数字很大,我唯一的猜测是在算法的某处发生了溢出。
好吧,这就是问题:算法和整个程序在保持&lt; = 4800万行文本时完美无缺。一旦我超越了segfault就会发生。我还确保读取数据有效,这意味着在读取有关3gb RAM的数据的过程中正在使用。 segfault在char数组的排序过程中肯定会发生。
那么为什么它可以处理多达48.000.000行的文本,为什么在拥有更多文本时会出现分段错误?我的错误在哪里?
答案 0 :(得分:2)
您的算法中有一个未考虑的边缘情况。
如果原始序列的 的底部(左边缘)分区遇到 no swaps (即每个值都是“更大 - 或 - 等于“比枢轴”,然后index
,从零(0
)开始,将保持原样。索引i
将一直走到尽头。然后将临时存储在最右侧插槽中的数据透视值交换到位(即cArr[0]
和cArr[right]
交换),然后从函数返回0
。换句话说,这个:
size_t pivot = partition(base, left, right, compar);
#pragma omp task
quicksort(base, left, pivot-1, compare);
// here ================^
执行时pivot
从前一次调用返回为零,将pivot-1
作为right
传递并导致下溢。这将为您提供完全您错误时获得的right
的值。 (在我曾经使用的每个平台上都是2 ^ 64-1。)
你需要考虑到这一点(或者永远不要让它发生在一起)。在代码中是否发生这种情况完全取决于使用left=0
处理的每个分区的内容。它可能不会在第一次,第二次等发生。但是,将正确的数据交换到不断减少的分区空间,最终可能发生。
未经测试,但值得一看
我首先不喜欢quicksort()的C实现中的left
和right
分区标记。该语言支持指针数学,因此使用 并围绕你知道具体的东西(基数和长度)。我没有测试过以下内容,只有一次曾经不得不处理OMP,但简化了,我的意思是这样的:
void quicksort(void* base, size_t len, int(*compar)(const void*, const void*))
{
if (len < 2)
return;
char* cArr = (char*)base;
char* pivotValue = cArr + ((size_t)TSIZE * (len - 1));
SWAP(cArr + ((size_t)TSIZE * (len / 2)), pivotValue, TSIZE);
size_t i;
size_t pivot = 0;
for (i = 0; i < len; ++i)
{
if (compar(cArr + ((size_t)TSIZE * i), ) < 0)
{
SWAP(cArr + ((size_t)TSIZE * i), cArr + ((size_t)TSIZE * pivot), (size_t)TSIZE);
++pivot;
}
}
SWAP(cArr + ((size_t)TSIZE * pivot), pivotValue, (size_t)TSIZE);
#pragma omp task
quicksort(cArr, pivot++, compar);
#pragma omp task
quicksort(cArr+((size_t)TSIZE * pivot), len-pivot, compar);
}
我希望很明显如何调用它。
答案 1 :(得分:0)
一次通过分区操作既简单又可爱,但它比需要更多更多交换。正如所写的那样,在一个小小的测试中,我发现它需要两倍半交换所需的数量!
'可爱'分区有两个问题:
当index
和i
相同时,值为&lt;枢轴值,然后它自己交换自己。如果交换费用昂贵,那么对“自我”的测试会节省一点。
当它交换时,它会将index
向前的值移动到i
,并踩到两者。如果index
到达该值,它可能必须再次向前交换 - 进行“额外”(不必要的)交换,在推进的“指数”之前随机改变值。
考虑对五个值进行分区:9 3 5 1 4
。首先,将选择5作为枢轴值,并与4交换,得到9 3 4 1 5
。然后,从index == i == left
开始,9大于枢轴,因此请离开index
并前进i
。现在3小于枢轴,所以我们交换9和3并推进index
和i
,得到3 9 4 1 5
。现在4小于枢轴,所以再次交换 ,将9向前移动,给出3 4 9 1 5
。并且1也小于枢轴,因此再次交换 ,给出3 4 1 9 5
。最后,将透视值交换到位可以完成提交3 4 1 5 9
的过程。
所以,这样做 3 交换以改变9,同时只需要 1 交换。
一种常见但不那么简单的方法是先从左侧开始,然后从右侧进行分区扫描,查找需要上下交换的值,以便以最小的交换次数完成此过程
我尝试了50个整数的向量,每个值从1..50随机选择。我运行了'可爱'分区和一个更“传统”的分区,并计算了超过20,000次试验的掉期。我得到了:
Average 'trad' swaps = 9.8
Average 'cute' swaps = 23.0
Average 'cute' selfs = 2.9
Average 'cute' extra = 13.0
其中'trad'最小化掉期数量。 “自我”是交换,其中index
== i
,而extras
是值多次转发的位置。 '可爱'互换计数包括'额外',但不包括'自我'(因为'自我'是微不足道的消除)。
交换计数包括交换枢轴值并再次交换它 - 因为这两种算法的相同。折扣这两个互换,'可爱'算法做了23.9 / 7.8或三次所需的交换(平均来说,在我的小测试中)。
因此,无论多么可爱,简单的一次性分区都是垃圾。
为了完整起见,这是一个更“传统”的分区:
/* 'left' and 'right' are indexes into 'data', and there are
* are 'right' - 'left' + 1 items in the partition.
*
* 'pivot' is the index of the chosen pivot-value, and is set
* to the (new) index for that when the partition completes.
*/
pv = data[pivot] ;
swap(data, pivot, right) ;
l = left ;
r = right ;
while (l < r)
{
--r ;
while ((l < r) && (data[l] <= pv))
++l ;
while ((l < r) && (data[r] >= pv))
--r ;
if (l == r)
{
if (data[r] < pv)
++r ;
break ;
} ;
swap(data, l, r) ;
++l ;
} ;
pivot = r ;
swap(data, pivot, right) ;