4

次のタスクには非常に高速なアルゴリズムが必要です。私はすでにそれを完成させるいくつかのアルゴリズムを実装していますが、それらはすべて私が必要とするパフォーマンスに対して遅すぎます. 最新の CPU でアルゴリズムを少なくとも 1 秒間に 100,000 回実行できるほど高速である必要があります。C++ で実装されます。

線上に開始座標と終了座標を持つ構造であるスパン/範囲を使用しています。

スパンの 2 つのベクトル (動的配列) があり、それらをマージする必要があります。1 つのベクトルは src で、もう 1 つのベクトルは dst です。ベクトルはスパン開始座標でソートされ、スパンは 1 つのベクトル内でオーバーラップしません。

src ベクトルのスパンは dst ベクトルのスパンとマージする必要があります。これにより、結果のベクトルがソートされ、オーバーラップがなくなります。すなわち。マージ中にオーバーラップが検出された場合、2 つのスパンが 1 つにマージされます。(2 つのスパンのマージは、構造内の座標を変更するだけの問題です。)

ここで、もう 1 つ問題があります。マージ中に src ベクトルのスパンを「拡張」する必要があります。これは、src 内のすべてのスパンの開始座標に定数が追加され、別の (より大きな) 定数が終了座標に追加されることを意味します。これは、src スパンが拡張された後にオーバーラップする可能性があることを意味します。


私がこれまでに到達したことは、完全にその場で行うことはできず、何らかの一時的な保管が必要であるということです. 合計された src と dst の要素数に対して線形時間で実行できるはずだと思います。

一時ストレージは、おそらくアルゴリズムの複数の実行間で共有できます。

私が試した 2 つの主なアプローチは遅すぎますが、次のとおりです。

  1. src のすべての要素を dst に追加し、追加する前に各要素を拡張します。次に、インプレース ソートを実行します。最後に、「読み取り」および「書き込み」ポインターを使用して結果のベクターを反復処理します。読み取りポインターは書き込みポインターの前に実行され、スパンが進むにつれてマージされます。すべての要素がマージされると (読み取りポインターが end に達すると)、dst は切り捨てられます。

  2. 一時的な作業ベクトルを作成します。src または dst から次の要素を繰り返し選択し、work-vector にマージすることにより、上記の単純なマージを実行します。完了したら、work-vector を dst にコピーして置き換えます。

最初の方法には、ソートが O(m+n) ではなく O((m+n)*log(m+n)) であり、多少のオーバーヘッドがあるという問題があります。また、dst ベクトルが実際に必要とするよりもはるかに大きくなる必要があることも意味します。

2番目には、大量のコピーとメモリの割り当て/割り当て解除という主な問題があります。

スパン/ベクターの保存/管理に使用されるデータ構造は、必要に応じて変更できます。

更新: データセットの大きさを言い忘れました。最も一般的なケースは、いずれかのベクトルの要素が 4 ~ 30 で、dst が空であるか、src と dst のスパン間に大量の重複があります。

4

9 に答える 9

8

リストをマージできるようにするには、少なくともすべてのデータをスキャンする必要があるため、絶対的な最良のケースの実行時間は O(m+n) であることがわかっています。これを考えると、2 番目の方法でそのような動作が得られるはずです。

ボトルネックが何であるかを調べるための 2 番目の方法をプロファイリングしましたか? 話しているデータの量によっては、指定された時間内に必要なことを実際に行うことが不可能である可能性は十分にあります。これを確認する 1 つの方法は、ループ内の各ベクトルのスパンのすべての開始値と終了値を合計し、その時間を計るなどの単純なことを行うことです。基本的にここでは、ベクトル内の各要素に対して最小限の作業を行っています。これにより、期待できる最高のパフォーマンスのベースラインが得られます。

さらに、stl swap メソッドを使用して要素ごとにベクトルをコピーすることを回避できます。また、要素をマージするときに配列の拡張がトリガーされないように、一時ベクトルを特定のサイズに事前に割り当てることができます。

システムで 2 つのベクトルを使用することを検討し、マージを行う必要があるときはいつでも、未使用のベクトルにマージしてからスワップします (これは、グラフィックスで使用されるダブル バッファリングに似ています)。これにより、マージを行うたびにベクトルを再割り当てする必要がなくなります。

ただし、最初にプロファイリングを行い、ボトルネックが何であるかを調べることをお勧めします。実際のマージ プロセスと比較して割り当てが最小限である場合は、それを高速化する方法を理解する必要があります。

ベクトルの生データに直接アクセスすることで、データにアクセスするたびに境界チェックを回避することで、さらに高速化できる可能性があります。

于 2008-09-18T02:26:16.877 に答える
0

src ベクトル スパンを広げると、最悪の場合、それらすべてがオーバーラップする可能性があるため、厳密に線形のソリューションは可能ではないと思います (追加する定数の大きさによって異なります)。

問題はアルゴリズムではなく、実装にある可能性があります。以前のソリューションのコードをプロファイリングして、どこに時間が費やされているかを確認することをお勧めします

推論:

3.2GHz で動作する Intel Core 2 Extreme QX9770 のような真に「最新の」CPU の場合、約 59,455 MIPS が期待できます。

100,000 個のベクトルの場合、各ベクトルを 594,550 回の命令で処理する必要があります。それはたくさんの指示です。

参照:ウィキペディア MIPS

さらに、src ベクトル スパンに定数を追加してもソート解除されないことに注意してください。そのため、src ベクトル スパンを個別に正規化してから、dst ベクトル スパンとマージできます。これにより、元のアルゴリズムの作業負荷が軽減されます

于 2008-09-18T02:25:38.157 に答える
0

私は常にスパンのベクトルをソートしたままにします。これにより、アルゴリズムの実装が非常に簡単になり、線形時間で実行できるようになります。

では、以下に基づいてスパンを並べ替えます。

  • 昇順でスパン最小
  • 降順で最大スパン

そのための関数を作成する必要があります。

次に、 std::set_union を使用してベクトルをマージします (続行する前に複数をマージできます)。

次に、同一の最小値を持つ連続するスパンのセットごとに、最初のものを保持し、残りを削除します (それらは最初のスパンのサブスパンです)。

次に、スパンをマージする必要があります。それは今でもかなり実行可能であり、線形時間で実現可能です。

OK、ここにトリックがあります。これをインプレースで実行しようとしないでください。1 つ以上の一時的なベクトルを使用します (事前に十分なスペースを確保してください)。最後に、 std::vector::swap を呼び出して、選択した入力ベクトルに結果を入れます。

これで十分だと思います。

于 2008-09-18T04:01:33.450 に答える
0

割り当てを繰り返さない 2 番目の方法はどうでしょうか。つまり、一時ベクトルを一度割り当てたら、二度と割り当てないということです。または、入力ベクトルが十分に小さい場合 (ただし、一定のサイズではない)、malloc の代わりに alloca を使用します。

また、スピードの観点から、コードがソートに CMOV を使用していることを確認することをお勧めします。これは、コードがマージソートの反復ごとに実際に分岐している場合:

if(src1[x] < src2[x])
    dst[x] = src1[x];
else
    dst[x] = src2[x];

分岐予測は 50% の確率で失敗し、パフォーマンスに大きな影響を与えます。条件付きの移動の方がはるかにうまくいく可能性が高いので、コンパイラがそれを行っていることを確認し、そうでない場合は、そうするように誘導してみてください。

于 2008-09-18T02:17:52.263 に答える
0

アプローチ 1 で言及した並べ替えは、2 つの入力リストが既に並べ替えられているため、(説明した対数線形から) 線形時間に短縮できます。マージソートのマージステップを実行するだけです。入力スパン ベクトルの適切な表現 (単一リンク リストなど) を使用すると、これをインプレースで行うことができます。

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

于 2008-09-18T02:22:54.340 に答える
0

1 は正解です。完全な並べ替えは、並べ替えられた 2 つのリストをマージするよりも遅くなります。

つまり、微調整 2 (またはまったく新しいもの) を見ています。

データ構造を双方向にリンクされたリストに変更すると、一定の作業スペースでそれらをマージできます。

リスト ノードに固定サイズのヒープ アロケーターを使用して、ノードごとのメモリ使用量を削減し、ノードがメモリ内で近接して配置される可能性を高め、ページ ミスを減らします。

リンク リストの結合を最適化するためのコードを、オンラインまたはお気に入りのアルゴリズムの本で見つけることができるかもしれません。リストのマージと同時にスパンの合体を行うために、これをカスタマイズする必要があります。

マージを最適化するには、最初に、反対側からの値ではなく、同じ側からの値の実行ごとに、各ノードを順番に挿入する代わりに、実行全体を一度に dst リストに挿入できることに注意してください。また、通常のリスト操作よりも、挿入ごとに 1 回の書き込みを節約できます。これは、後でパッチを適用することを知って、末尾を「ぶら下げ」たままにしておくことで実現できます。また、アプリ内の他の場所で削除を行わない限り、リストは単一リンクにすることができます。つまり、ノードごとに 1 回の書き込みを意味します。

10 マイクロ秒の実行時間については、種類は n と m に依存します...

于 2008-09-18T02:42:44.747 に答える
0

あなたのターゲットシステムは何ですか?マルチコアですか?もしそうなら、このアルゴリズムをマルチスレッド化することを検討できます

于 2008-09-18T16:38:42.723 に答える
0

最新の実装がまだ十分に高速でない場合は、別のアプローチを検討する必要が生じる可能性があります。

この関数の出力を何に使用していますか?

于 2008-09-18T02:54:28.837 に答える
0

ニーズに合わせて、このアルゴリズムのためだけに新しいコンテナー クラスを作成しました。これにより、プログラムの周りの他のコードを調整する機会が得られ、同時に速度が少し向上しました.

これは、STL ベクトルを使用した古い実装よりも大幅に高速ですが、それ以外は基本的に同じものでした。しかし、高速ではありますが、まだ十分な速度ではありません...残念ながら.

プロファイリングでは、本当のボトルネックはもはやわかりません。MSVC プロファイラーは、間違った呼び出しに「責任」を与えることがあるようで (おそらく、同じ実行でも実行時間が大きく異なる可能性があります)、ほとんどの呼び出しは 1 つの大きな隙間に合体します。

生成されたコードの逆アセンブルを見ると、生成されたコードに非常に大量のジャンプがあることがわかります。これが現在の速度低下の主な理由である可能性があると思います。

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();
}
于 2008-09-18T03:15:16.010 に答える