我需要一个非常快速的算法来执行以下任务。我已经实现了几种完成它的算法,但它们对于我需要的性能来说太慢了。它应该足够快,以至于算法在现代CPU上每秒至少运行100,000次。它将在C ++中实现。
我正在使用跨距/范围,一个在一条线上有一个起点和终点坐标的结构。
我有两个跨度向量(动态数组),我需要合并它们。一个向量是src而另一个是dst。矢量按跨度起始坐标排序,跨度不在一个矢量内重叠。
src向量中的跨距必须与dst向量中的跨距合并,这样得到的向量仍然是有序的并且没有重叠。 IE浏览器。如果在合并期间检测到重叠,则将两个跨度合并为一个。 (合并两个跨度只是改变结构中的坐标。)
现在,还有一个问题,src向量中的跨度必须在合并期间“加宽”。这意味着常量将添加到开始,另一个(更大)常量添加到src中每个跨度的结束坐标。这意味着在src spans加宽后它们可能会重叠。
到目前为止我所得到的是它不能完全就地完成,需要某种临时存储。我认为它应该在线性时间内超过src和dst求和的元素数量。
任何临时存储都可以在算法的多次运行之间共享。
我尝试过的两种主要方法是:
将src的所有元素追加到dst,在追加每个元素之前加宽它们。然后运行就地排序。最后使用“读”和“写”指针迭代结果向量,读指针在写指针之前运行,合并跨越。当所有元素已合并(读指针到达结尾)时,dst将被截断。
创建临时工作向量。通过重复从src或dst中选择下一个元素并合并到工作向量中,如上所述进行简单的合并。完成后,将工作向量复制到dst,替换它。
第一种方法的问题是排序是O((m + n)* log(m + n))而不是O(m + n)并且有一些开销。这也意味着dst向量必须比实际需要的大得多。
第二个主要问题是大量复制并再次分配/释放内存。
如果您认为需要,可以更改用于存储/管理跨度/向量的数据结构。
更新:忘了说数据集有多大。最常见的情况是两个向量中的4到30个元素,并且dst为空或者src和dst中的跨度之间存在大量重叠。
答案 0 :(得分:8)
我们知道绝对最佳情况运行时为O(m + n),这是因为您至少必须扫描所有数据才能合并列表。鉴于此,您的第二种方法应该给您这种行为。
你有没有想过你的第二种方法来找出瓶颈是什么?根据您所讨论的数据量,很可能实际上无法在指定的时间内完成您想要的操作。验证这一点的一种方法是做一些简单的事情,例如总结循环中每个向量中跨距的所有起始值和结束值,以及时间。基本上,您在向量中为每个元素执行最少量的工作。这将为您提供可获得的最佳性能基准。
除此之外,您可以避免使用stl swap方法逐个元素地复制矢量,并且可以将临时矢量预分配到特定大小,以避免在合并元素时触发数组的扩展。 / p>
您可能会考虑在系统中使用2个向量,并且只要需要进行合并,就会合并到未使用的向量中,然后交换(这类似于图形中使用的双缓冲)。这样,每次进行合并时都不必重新分配矢量。
但是,您最好先进行分析,然后找出您的瓶颈。如果分配与实际合并过程相比是最小的,那么您需要弄清楚如何更快地进行分配。
一些可能的额外加速可能来自直接访问矢量原始数据,这避免了每次访问数据的边界检查。
答案 1 :(得分:0)
没有重复分配的第二种方法怎么样 - 换句话说,分配你的临时向量一次,永远不再分配它?或者,如果输入向量足够小(但不是常量),只需使用alloca而不是malloc。
此外,就速度而言,您可能希望确保您的代码使用CMOV进行排序,因为如果代码实际上是为mergesort的每次迭代分支:
if(src1[x] < src2[x])
dst[x] = src1[x];
else
dst[x] = src2[x];
分支预测将在50%的时间内失败,这将对性能产生巨大影响。条件移动可能会做得更好,所以要确保编译器正在这样做,如果没有,请尝试哄骗它这样做。
答案 2 :(得分:0)
您在方法1中提到的排序可以缩减为线性时间(从描述它的log-linear),因为两个输入列表已经排序。只需执行merge-sort的合并步骤。通过适当的输入范围向量表示(例如单链表),可以就地完成。
答案 3 :(得分:0)
我不认为严格的线性解决方案是可能的,因为加宽src矢量跨度可能在最坏的情况下导致它们全部重叠(取决于您添加的常量的大小)
问题可能在于实现,而不是在算法中;我建议您为以前的解决方案分析代码,看看花费的时间
推理:
对于真正的“现代”CPU,如运行在3.2GHz的Intel Core 2 Extreme QX9770,可以预期大约59,455 MIPS
对于100,000个向量,您必须在594,550个指令中处理每个向量。这是很多指示。
另外,请注意,向src向量跨度添加常量不会对它们进行解除排序,因此可以独立地对src向量跨度进行标准化,然后将它们与dst向量跨度合并;这应该减少原始算法的工作量
答案 4 :(得分:0)
1是正确的 - 完全排序比合并两个排序列表慢。
所以你正在考虑调整2(或者全新的东西)。
如果将数据结构更改为双向链接列表,则可以将它们合并到恒定的工作空间中。
为列表节点使用固定大小的堆分配器,既可以减少每个节点的内存使用量,又可以提高节点在内存中靠近的可能性,从而减少页面未命中。
您可以在线或在您喜欢的算法手册中找到代码,以优化链接列表合并。您需要对其进行自定义,以便在列表合并的同时进行跨度合并。
要优化合并,首先要注意的是,对于从同一侧出来的每次运行值而没有来自另一侧的值,您可以一次性将整个运行插入到dst列表中,而不是依次插入每个节点。并且你可以在正常列表操作中保存每次插入一次写入,留下结尾“悬空”,知道你将在以后修补它。如果您不在应用程序中的任何其他位置删除,则列表可以单链接,这意味着每个节点一次写入。
至于10微秒的运行时间 - 取决于n和m ......
答案 5 :(得分:0)
如果您最近的实施仍然不够快,您最终可能不得不考虑其他方法。
您使用此功能的输出是什么?
答案 6 :(得分:0)
我为这个算法编写了一个新的容器类,根据需要量身定制。这也让我有机会调整我的程序周围的其他代码,同时提高了一点速度。
这比使用STL向量的旧实现快得多,但其他方面基本相同。但不幸的是,它虽然速度更快,但仍然不够快......不幸。
分析不再揭示真正的瓶颈是什么。 MSVC分析器似乎有时会对错误的调用进行“责备”(假设相同的运行分配了大量不同的运行时间),并且大多数调用都会合并到一个大的缝隙中。
查看生成的代码的反汇编表明生成的代码中有大量的跳转,我认为这可能是现在缓慢的主要原因。
class SpanBuffer {
private:
int *data;
size_t allocated_size;
size_t count;
inline void EnsureSpace()
{
if (count == allocated_size)
Reserve(count*2);
}
public:
struct Span {
int start, end;
};
public:
SpanBuffer()
: data(0)
, allocated_size(24)
, count(0)
{
data = new int[allocated_size];
}
SpanBuffer(const SpanBuffer &src)
: data(0)
, allocated_size(src.allocated_size)
, count(src.count)
{
data = new int[allocated_size];
memcpy(data, src.data, sizeof(int)*count);
}
~SpanBuffer()
{
delete [] data;
}
inline void AddIntersection(int x)
{
EnsureSpace();
data[count++] = x;
}
inline void AddSpan(int s, int e)
{
assert((count & 1) == 0);
assert(s >= 0);
assert(e >= 0);
EnsureSpace();
data[count] = s;
data[count+1] = e;
count += 2;
}
inline void Clear()
{
count = 0;
}
inline size_t GetCount() const
{
return count;
}
inline int GetIntersection(size_t i) const
{
return data[i];
}
inline const Span * GetSpanIteratorBegin() const
{
assert((count & 1) == 0);
return reinterpret_cast<const Span *>(data);
}
inline Span * GetSpanIteratorBegin()
{
assert((count & 1) == 0);
return reinterpret_cast<Span *>(data);
}
inline const Span * GetSpanIteratorEnd() const
{
assert((count & 1) == 0);
return reinterpret_cast<const Span *>(data+count);
}
inline Span * GetSpanIteratorEnd()
{
assert((count & 1) == 0);
return reinterpret_cast<Span *>(data+count);
}
inline void MergeOrAddSpan(int s, int e)
{
assert((count & 1) == 0);
assert(s >= 0);
assert(e >= 0);
if (count == 0)
{
AddSpan(s, e);
return;
}
int *lastspan = data + count-2;
if (s > lastspan[1])
{
AddSpan(s, e);
}
else
{
if (s < lastspan[0])
lastspan[0] = s;
if (e > lastspan[1])
lastspan[1] = e;
}
}
inline void Reserve(size_t minsize)
{
if (minsize <= allocated_size)
return;
int *newdata = new int[minsize];
memcpy(newdata, data, sizeof(int)*count);
delete [] data;
data = newdata;
allocated_size = minsize;
}
inline void SortIntersections()
{
assert((count & 1) == 0);
std::sort(data, data+count, std::less<int>());
assert((count & 1) == 0);
}
inline void Swap(SpanBuffer &other)
{
std::swap(data, other.data);
std::swap(allocated_size, other.allocated_size);
std::swap(count, other.count);
}
};
struct ShapeWidener {
// How much to widen in the X direction
int widen_by;
// Half of width difference of src and dst (width of the border being produced)
int xofs;
// Temporary storage for OverlayScanline, so it doesn't need to reallocate for each call
SpanBuffer buffer;
inline void OverlayScanline(const SpanBuffer &src, SpanBuffer &dst);
ShapeWidener(int _xofs) : xofs(_xofs) { }
};
inline void ShapeWidener::OverlayScanline(const SpanBuffer &src, SpanBuffer &dst)
{
if (src.GetCount() == 0) return;
if (src.GetCount() + dst.GetCount() == 0) return;
assert((src.GetCount() & 1) == 0);
assert((dst.GetCount() & 1) == 0);
assert(buffer.GetCount() == 0);
dst.Swap(buffer);
const int widen_s = xofs - widen_by;
const int widen_e = xofs + widen_by;
size_t resta = src.GetCount()/2;
size_t restb = buffer.GetCount()/2;
const SpanBuffer::Span *spa = src.GetSpanIteratorBegin();
const SpanBuffer::Span *spb = buffer.GetSpanIteratorBegin();
while (resta > 0 || restb > 0)
{
if (restb == 0)
{
dst.MergeOrAddSpan(spa->start+widen_s, spa->end+widen_e);
--resta, ++spa;
}
else if (resta == 0)
{
dst.MergeOrAddSpan(spb->start, spb->end);
--restb, ++spb;
}
else if (spa->start < spb->start)
{
dst.MergeOrAddSpan(spa->start+widen_s, spa->end+widen_e);
--resta, ++spa;
}
else
{
dst.MergeOrAddSpan(spb->start, spb->end);
--restb, ++spb;
}
}
buffer.Clear();
}
答案 7 :(得分:0)
我会一直保持我的跨度矢量排序。这使得实现算法变得更容易 - 并且可以在线性时间内完成。
好的,所以我根据以下内容对间距进行排序:
您需要创建一个功能来执行此操作。
然后我使用std :: set_union来合并向量(你可以在继续之前合并多个)。
然后对于每个具有相同最小值的连续跨距组,保留第一个并移除其余的(它们是第一个跨度的子跨度)。
然后你需要合并你的跨度。这应该是非常可行的,并且在线性时间内是可行的。
好的,现在就是诀窍。不要试图就地做到这一点。使用一个或多个临时向量(并提前保留足够的空间)。然后在最后,调用std :: vector :: swap将结果放入您选择的输入向量中。
我希望这足以让你前进。
答案 8 :(得分:0)
你的目标系统是什么?它是多核的吗?如果是这样,你可以考虑多线程这个算法