近似排序(数组/向量),可预测的运行时间

时间:2014-01-29 14:58:20

标签: c++ algorithm sorting

背景:

考虑到时间限制,我需要处理数十万个事件(产生结果)。时钟字面上滴答作响,当计时器触发时,必须刷新在该点完成的任何操作。

那个时间尚未准备好的东西要么被丢弃(取决于重要性度量),要么在下一个时间段内处理(具有“重要性提升”,即为重要性度量添加常量)。
理想情况下,CPU比需要的速度快得多,并且整个集合在时间片结束之前已经准备好了很长时间。不幸的是,世界很少是理想的,在您知道之前,“数十万”变成“数千万”。

事件被添加到队列的后面(它实际上是一个向量),并且在相应的下一个量子期间从前面处理(因此程序总是处理最后一个量子的输入)。

但是,并非所有事件都同样重要。如果可用时间不够,最好放弃不重要的事件而不是重要的事件(这不是一个严格的要求,因为重要的事件将被复制到下一个时间量子的队列,但这样做会进一步增加负载所以它不是一个完美的解决方案。)

使用的显而易见的事情当然是优先级队列/堆。不幸的是,堆积100k元素并不是一个自由操作(或并行),然后我最终将对象放在一些非显而易见且不一定是缓存友好的内存位置,并且从优先级队列中提取元素不会很好地平行化。
我真正喜欢的有点像一个被排序或至少“稍微近似排序”的矢量,之后可以顺序遍历。这将简单地允许我创建例如12个线程(或任何其他数字,每个CPU一个)处理,例如每个范围的1/64(或另一个大小),从前端到末端缓慢前进,最终丢弃/推迟剩下的 - 这将是可以丢弃的重要事件。

使用std::sort简单地对整个范围进行排序将是最简单,最直接的解决方案。但是,对项目进行排序所需的时间减少了在固定时间预算内实际处理元素的可用时间,而排序时间大部分是单CPU时间(并行排序也不是很好)。
此外,做一个完美的排序(这不是真正需要的)可能会带来最坏的情况复杂性,而近似排序理想情况下应该以最佳状态执行,并且具有非常可预测的成本。

TL;博士

所以,我正在寻找的是一种方法,只对大约的数组/向量进行排序,但速度快,并且具有可预测(或保证)的运行时。

排序键是一个通常介于10和1000之间的小整数。被推迟到下一次量子可能会增加(“优先级提升”)该值的一小部分,例如100或200。

different question应该使用“主观比较”进行近似排序(?) shell sort 。在各种排序演示applet上,似乎至少对于那些典型的“随机随机”输入,shell排序确实可以进行“近似排序”,对于数据的3-4次传递看起来并不太糟糕(和至少读取抽头是严格顺序的)。不幸的是,选择能够很好地工作的间隙值似乎是一种黑色艺术,而运行时估计似乎也涉及大量调查水晶球。

梳子排序具有相对较大的收缩因子(例如2或3?)似乎也很诱人,因为它严格按顺序访问内存(在两个水龙头上)并且能够远离元素快速的远距离。再次,从排序演示小程序判断,似乎3-4遍已经给出了一个相当合理的“近似排序”。

我想到了

MSD基数排序,虽然我不确定如果给出典型的16/32位整数,其中大部分最重要的位都为零,它会如何执行!人们可能不得不做一个初始传递来找到整个集合中最重要的位,然后是2-3个实际的排序传递?

使用我提到的算法之一,是否有更好的算法或众所周知的工作方法?

6 个答案:

答案 0 :(得分:3)

我想到的是迭代向量,如果某个事件不太重要,请不要处理它,而是将它放在一边。一旦读完整个矢量,就看看放在一边的事件。当然,您可以使用具有不同优先级的多个存储桶。并且只存储那里的引用,你不想移动数兆字节的数据。 (根据达蒙的要求,现在发布了答案)

答案 1 :(得分:3)

为每个优先级使用单独的向量。然后你不需要对它们进行排序。

答案 2 :(得分:3)

听起来像一个很好的例子,近似排序算法很有用。

回到过去的十年,Chazelle开发了一个很好的数据结构,有点像堆。关键的区别在于时间复杂度。它具有所有重要操作的恒定时间,例如,插入,删除,找到最低元素等。

这种数据结构的技巧是,它通过允许排序顺序中的某些错误来打破O(n * log n)复杂性障碍。

对我来说,听起来就像你需要的那样。数据结构称为软堆,并在维基百科上解释:

https://en.wikipedia.org/wiki/Soft_heap

还有其他算法允许一些有利于速度的错误。如果您谷歌搜索近似排序算法

,您会找到它们

如果您尝试该算法,请在实践中提供一些反馈。我真的很想知道算法在实践中的表现。

答案 3 :(得分:1)

听起来您想使用std::partition:将您感兴趣的部分移到前面,将其他部分移到后面。它的复杂性大约为O(n),但它对缓存友好,所以它可能比排序快很多。

答案 4 :(得分:1)

如果处理事件(例如每个时间量程为128K)的“带宽”有限,则可以使用std::nth_element选择128K(减去因计算而丢失的一些百分比)最有希望的事件(假设在operator<时间内,您有O(N)比较优先级。然后你并行处理它们,当你完成后,你重新确定剩余部分的优先级(再次在O(N)时间内)。

std::vector<Event> events;
auto const guaranteed_bandwidth = 1<<17; // 128K is always possible to process

if (events.size() <= guaranteed_bandwidth) {
    // let all N workers loose on [begin(events), end(events)) range
} else {
    auto nth = guaranteed_bandwidth * loss_from_nth_element;
    std::nth_element(begin(events), begin(events) + nth);
    // let all N workers loose on [begin(events), nth) range

    // reprioritize [nth, end(events)) range and append to events for next time quantum         
}

这可以保证在达到带宽阈值的情况下,首先处理最有价值的元素。您甚至可以通过穷人的并行化来加速nth_element(例如,让N个工作人员为并行的小M计算M * 128K / N最佳元素,然后进行最终合并,然后进行另一个{ M * 128K元素上的{1}}。

唯一的缺点是,如果您的系统真的超载(数十亿事件,可能是由于某些DOS攻击),它可能需要比整个量程运行nth_element更多(即使是准平行的)你实际上什么都不处理。但是,如果每个事件的处理时间比优先级比较(比如十几个周期)要大得多(比如几千个周期),那么这不应该在常规负载下发生。

注意:出于性能原因,将指针/索引排序到主事件向量当然更好,为简洁起见,此处未显示。

答案 5 :(得分:0)

如果您有N个工作线程,请为每个工作线程提供原始未排序数组的1 / N.工作人员将要做的第一件事是你的近似快速排序算法,优先选择它的阵列的各个部分。然后,他们可以按顺序处理他们的数组 - 大致首先执行更高优先级的项目,并且还非常缓存友好。这样,您不会尝试对整个阵列进行排序,甚至尝试对整个阵列进行近似排序;什么小排序,是完全并行化的。单独分拣10件比分拣整件便宜得多。

如果要处理的项目的优先级是随机分布的,这将最有效。如果对它们有一些排序,你最终会被一个被高优先级项目淹没或缺乏的线程处理。