帮助合并向量的算法

时间:2008-09-18 02:12:57

标签: c++ algorithm optimization graphics vector

我需要一个非常快速的算法来执行以下任务。我已经实现了几种完成它的算法,但它们对于我需要的性能来说太慢了。它应该足够快,以至于算法在现代CPU上每秒至少运行100,000次。它将在C ++中实现。

我正在使用跨距/范围,一个在一条线上有一个起点和终点坐标的结构。

我有两个跨度向量(动态数组),我需要合并它们。一个向量是src而另一个是dst。矢量按跨度起始坐标排序,跨度不在一个矢量内重叠。

src向量中的跨距必须与dst向量中的跨距合并,这样得到的向量仍然是有序的并且没有重叠。 IE浏览器。如果在合并期间检测到重叠,则将两个跨度合并为一个。 (合并两个跨度只是改变结构中的坐标。)

现在,还有一个问题,src向量中的跨度必须在合并期间“加宽”。这意味着常量将添加到开始,另一个(更大)常量添加到src中每个跨度的结束坐标。这意味着在src spans加宽后它们可能会重叠。


到目前为止我所得到的是它不能完全就地完成,需要某种临时存储。我认为它应该在线性时间内超过src和dst求和的元素数量。

任何临时存储都可以在算法的多次运行之间共享。

我尝试过的两种主要方法是:

  1. 将src的所有元素追加到dst,在追加每个元素之前加宽它们。然后运行就地排序。最后使用“读”和“写”指针迭代结果向量,读指针在写指针之前运行,合并跨越。当所有元素已合并(读指针到达结尾)时,dst将被截断。

  2. 创建临时工作向量。通过重复从src或dst中选择下一个元素并合并到工作向量中,如上所述进行简单的合并。完成后,将工作向量复制到dst,替换它。

  3. 第一种方法的问题是排序是O((m + n)* log(m + n))而不是O(m + n)并且有一些开销。这也意味着dst向量必须比实际需要的大得多。

    第二个主要问题是大量复制并再次分配/释放内存。

    如果您认为需要,可以更改用于存储/管理跨度/向量的数据结构。

    更新:忘了说数据集有多大。最常见的情况是两个向量中的4到30个元素,并且dst为空或者src和dst中的跨度之间存在大量重叠。

9 个答案:

答案 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的合并步骤。通过适当的输入范围向量表示(例如单链表),可以就地完成。

http://en.wikipedia.org/wiki/Merge_sort

答案 3 :(得分:0)

我不认为严格的线性解决方案是可能的,因为加宽src矢量跨度可能在最坏的情况下导致它们全部重叠(取决于您添加的常量的大小)

问题可能在于实现,而不是在算法中;我建议您为以前的解决方案分析代码,看看花费的时间

推理:

对于真正的“现代”CPU,如运行在3.2GHz的Intel Core 2 Extreme QX9770,可以预期大约59,455 MIPS

对于100,000个向量,您必须在594,550个指令中处理每个向量。这是很多指示。

参考:wikipedia MIPS

另外,请注意,向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)

你的目标系统是什么?它是多核的吗?如果是这样,你可以考虑多线程这个算法