【问题标题】:Quicksort in C with huge amount of data / memory具有大量数据/内存的 C 中的快速排序
【发布时间】:2014-07-23 20:21:53
【问题描述】:

我有以下代码设置:

#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。
我也在尝试并行化快速排序算法。代码行随着我使用的内核数量而增长,例如1 核 = 2400 万,2 核 = 4800 万,依此类推。

什么已经在起作用
只要文件大小保持在 4800 万行文本以下,工作已经在使用 1 到 8 个内核对文件进行排序。

我的问题是什么
我的问题是,一旦我尝试对包含 7200 万行或更多文本的文件进行排序,快速排序算法就会遇到分段错误。我已经尽我所能用gdb调试了代码,出错的代码是这一行:

SWAP(&cArr[(size_t)TSIZE * i], &cArr[(size_t)TSIZE * index], (size_t)TSIZE);

是for循环中partition函数中的swap调用。我还可以看到,此时变量“right”的值为 18446744073709551615 (2^64-1),这是导致分段错误的原因。 “正确”的最大值应该是 TSIZE * TNUM * CORES。由于这个数字很大,我唯一的猜测是算法中的某个地方发生了溢出。

好吧,这里有个问题:算法和整个程序在保持

那么,为什么它可以处理多达 48.000.000 行的文本,以及为什么在拥有更多文本时会出现段错误?我的错在哪里?

【问题讨论】:

  • 会不会是因为你在除以2之前左右相加,溢出了?
  • 对此,请尝试left + (right - left)/2(即使不是您的问题,您也应该这样做)。
  • @WhozCraig 那是正确的表达方式,正试图为它找到一个确定的来源。
  • (left+right)/2 不太可能是问题的原因,因为段错误发生在未使用 pivotIndex 的 for 循环中的交换处。感谢 WhozCraig 的建议。
  • 我认为@LasseV.Karlsen 有答案,但您还应该考虑交换字符串列表 pointers 而不是实际的字符串本身。这将使您的代码运行得更快 — 只需 3 个处理器操作码即可交换指针,而您的 SWAP 宏将需要数十个。

标签: c algorithm sorting segmentation-fault quicksort


【解决方案1】:

您的算法中有一个未考虑到的边缘情况。

如果原始序列的底部(左边缘)分区曾经遇到没有交换(即每个值都是“大于或-等于” 比枢轴),然后从零开始的 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 分区标记。该语言支持指针数学,因此请使用 that 并围绕您知道的具体事物(基数和长度)兜售。我没有测试过以下内容,并且只有一次不得不处理 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);
}

我希望它是如何调用的很明显。

【讨论】:

  • 感谢您的详细回答。不幸的是,我现在无法对此进行测试,因为我没有必要的数据和硬件。一旦我可以,我会告诉你它是否有效。
  • @Roter_Fuchs 不用担心。对不起,如果样品不能正常工作(我不知道 omp 并且手头没有任何东西可以测试)但是,我希望你能理解这个想法。如果有机会,使用 base、len 和指针数学确实是这样做的方法,但对您来说,最简单的解决方法可能是在第一次递归之前if(pivot &gt; 0)。
  • 我可以试一试,这确实是你描述的边缘情况有问题。再次感谢。
【解决方案2】:

一次性分区操作简单而可爱,但它比它需要的交换更多。正如所写,在一个小测试中我发现它两倍半所需的交换次数!

“可爱”分区有两个问题:

  1. 当index 和i 相同,且值为

  2. 当它交换时,它将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' 最小化掉期的数量。 “selfs”是交换,其中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) ;

【讨论】:

    猜你喜欢
    • 2019-11-05
    • 2018-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多