使用C ++在NxN数组中查找M个最大元素的优化方法

时间:2011-08-19 19:04:50

标签: c++ c caching optimization sorting

我需要一种快速的方法来查找NxN阵列中M个最大元素的2D位置和值。

现在我正在这样做:

struct SourcePoint {
    Point point;
    float value;
}

SourcePoint* maxValues = new SourcePoint[ M ];

maxCoefficients = new SourcePoint*[
for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
        float sample = arr[i][j];
        if (sample > maxValues[0].value) {
            int q = 1;
            while ( sample > maxValues[q].value && q < M ) {
                maxValues[q-1] = maxValues[q];      // shuffle the values back
                 q++;
            }
            maxValues[q-1].value = sample;
            maxValues[q-1].point = Point(i,j);
        }
    }
}

Point结构只有两个整数 - x和y。

此代码基本上是对值的插入排序.maxValues [0]始终包含具有最低值的SourcePoint,该值仍然保持在目前为止受到鼓舞的前M个值中。如果样本&lt; = maxValues,我们不做任何事情,这为我们提供了快速而轻松的救助。我遇到的问题是每次找到新的更好的值时都会进行改组。它一直向下运行maxValues,直到它找到它的位置,洗牌maxValues中的所有元素为自己腾出空间。

我已经准备好了解SIMD解决方案或缓存优化问题,因为看起来有一些缓存发生冲突。降低此操作的成本将极大地影响我的整体算法的性能,因为这被称为多次,占我总体成本的60-80%。

我尝试过使用std :: vector和make_heap,但我认为创建堆的开销超过了堆操作的节省。这可能是因为M和N通常不大。 M通常为10-20和N 10-30(NxN 100-900)。问题是这个操作被反复调用,无法预先计算。

我只想过预加载maxValues的前M个元素,这可能会带来一些小的节省。在当前算法中,前M个元素保证一直向下移动,只是为了初始填充maxValues。

非常感谢优化专家提供的任何帮助:)

10 个答案:

答案 0 :(得分:5)

您可以尝试一些想法。在N = 100和M = 15的一些快速测试中,我能够在VC ++ 2010中将它提高约25%,但是自己测试一下,看看它们是否对你的情况有帮助。根据实际使用情况/数据和编译器优化,其中一些更改可能没有甚至是负面影响。

  • 除非您需要,否则每次都不要分配新的maxValues数组。使用堆栈变量而不是动态分配可以获得+ 5%。
  • g_Source[i][j]更改为g_Source[j][i]可以获得一点点(不像我想的那么多)。
  • 使用底部列出的结构SourcePoint1让我获得了另外几个百分点。
  • 大约+ 15%的最大收益是用sample替换局部变量g_Source[j][i]。编译器可能足够聪明,可以优化对数组的多次读取,如果使用局部变量,它就无法完成。
  • 尝试一个简单的二进制搜索可以让我减少几个百分点。对于较大的M / N,您可能会看到一个好处。
  • 如果可能的话,尝试将源数据保持在arr[][]排序,即使只是部分排序。理想情况下,您希望在创建源数据的同时生成maxValues[]
  • 查看数据的创建/存储/组织方式可能会为您提供模式或信息,以减少生成maxValues[]数组的时间。例如,在最好的情况下,您可以提出一个公式,为您提供前M个坐标,而无需迭代和排序。

以上代码:

struct SourcePoint1 {
     int x;
     int y;
     float value;
     int test;       //Play with manual/compiler padding if needed
};

答案 1 :(得分:4)

如果你想在这一点上进行微观优化,那么简单的第一步应该是摆脱Point并将两个维度都放入一个int中。这减少了你需要转移的数据量,使SourcePoint降低到两个长的幂,这简化了索引。

另外,你确定保持列表排序比简单地重新计算每次将旧的最低点移出后哪个元素是新的最低要好吗?

答案 2 :(得分:4)

(更新时间:22:37 UTC 2011-08-20)

我提出了一个固定大小的二进制最小堆,它包含M个最大元素(但仍以最小堆顺序!)。 在实践中它可能不会更快,因为我认为OPs插入排序可能具有不错的真实世界性能(至少当考虑到该线程中其他后期的推荐时)。

在失败的情况下查找应该是恒定时间:如果当前元素小于堆的最小元素(包含最多M个元素),我们可以直接拒绝它。

如果事实证明我们的元素大于堆的当前最小值(第M个最大元素),我们提取(丢弃)前一个min并插入新元素。

如果按排序顺序需要元素,则可以在之后对堆进行排序。

首次尝试最小化的C ++实现:

template<unsigned size, typename T>
class m_heap {
private: 
    T nodes[size];
    static const unsigned last = size - 1;

    static unsigned parent(unsigned i) { return (i - 1) / 2; }
    static unsigned left(unsigned i) { return i * 2; }
    static unsigned right(unsigned i) { return i * 2 + 1; }

    void bubble_down(unsigned int i) {
        for (;;) { 
            unsigned j = i;
            if (left(i) < size && nodes[left(i)] < nodes[i])
                j = left(i);
            if (right(i) < size && nodes[right(i)] < nodes[j])
                j = right(i);
            if (i != j) {
                swap(nodes[i], nodes[j]);
                i = j;
            } else {
                break;
            }
        }
    }

    void bubble_up(unsigned i) {
        while (i > 0 && nodes[i] < nodes[parent(i)]) {
            swap(nodes[parent(i)], nodes[i]);
            i = parent(i);
        }
    }

public:
    m_heap() {
        for (unsigned i = 0; i < size; i++) {
            nodes[i] = numeric_limits<T>::min();
        }
    }

    void add(const T& x) {
        if (x < nodes[0]) {
            // reject outright 
            return;
        }
        nodes[0] = x;
        swap(nodes[0], nodes[last]);
        bubble_down(0);
    }
};

小测试/使用案例:

#include <iostream>
#include <limits>
#include <algorithm>
#include <vector>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

using namespace std;

// INCLUDE TEMPLATED CLASS FROM ABOVE

typedef vector<float> vf;
bool compare(float a, float b) { return a > b; }

int main()
{
    int N = 2000;
    vf v;
    for (int i = 0; i < N; i++) v.push_back( rand()*1e6 / RAND_MAX);

    static const int M = 50;
    m_heap<M, float> h;
    for (int i = 0; i < N; i++) h.add( v[i] );

    sort(v.begin(), v.end(), compare);

    vf heap(h.get(), h.get() + M); // assume public in m_heap: T* get() { return nodes; }
    sort(heap.begin(), heap.end(), compare);

    cout << "Real\tFake" << endl;
    for (int i = 0; i < M; i++) {
        cout << v[i] << "\t" << heap[i] << endl;
        if (fabs(v[i] - heap[i]) > 1e-5) abort();
    }
}

答案 3 :(得分:2)

您正在寻找priority queue

template < class T, class Container = vector<T>,
       class Compare = less<typename Container::value_type> > 
       class priority_queue;

您需要确定要使用的最佳基础容器,并可能定义Compare函数来处理您的Point类型。

如果要对其进行优化,可以在自己的工作线程中对矩阵的每一行运行一个队列,然后运行一个算法来选择队列前端的最大项,直到你有M个元素为止。

答案 4 :(得分:2)

快速优化是为您的maxValues数组添加标记值。如果maxValues[M].value等于std::numeric_limits<float>::max(),那么您可以在while循环条件中消除q < M测试。

答案 5 :(得分:1)

一个想法是在对NxN数组的简单一维引用序列中使用std::partial_sort算法。您可能还可以为后续调用缓存此引用序列。我不知道它的表现如何,但值得一试 - 如果它运作得足够好,你就没有那么多的“魔力”了。特别是,您不要求微观优化。

考虑这个展示:

#include <algorithm>
#include <iostream>
#include <vector>

#include <stddef.h>

static const int M = 15;
static const int N = 20;

// Represents a reference to a sample of some two-dimensional array
class Sample
{
public:
    Sample( float *arr, size_t row, size_t col )
        : m_arr( arr ),
        m_row( row ),
        m_col( col )
    {
    }

    inline operator float() const {
        return m_arr[m_row * N + m_col];
    }

    bool operator<( const Sample &rhs ) const {
        return (float)other < (float)*this;
    }

    int row() const {
        return m_row;
    }

    int col() const {
        return m_col;
    }

private:
    float *m_arr;
    size_t m_row;
    size_t m_col;
};

int main()
{
    // Setup a demo array
    float arr[N][N];
    memset( arr, 0, sizeof( arr ) );

    // Put in some sample values
    arr[2][1] = 5.0;
    arr[9][11] = 2.0;
    arr[5][4] = 4.0;
    arr[15][7] = 3.0;
    arr[12][19] = 1.0;

    //  Setup the sequence of references into this array; you could keep
    // a copy of this sequence around to reuse it later, I think.
    std::vector<Sample> samples;
    samples.reserve( N * N );
    for ( size_t row = 0; row < N; ++row ) {
        for ( size_t col = 0; col < N; ++col ) {
            samples.push_back( Sample( (float *)arr, row, col ) );
        }
    }

    // Let partial_sort find the M largest entry
    std::partial_sort( samples.begin(), samples.begin() + M, samples.end() );

    // Print out the row/column of the M largest entries.
    for ( std::vector<Sample>::size_type i = 0; i < M; ++i ) {
        std::cout << "#" << (i + 1) << " is " << (float)samples[i] << " at " << samples[i].row() << "/" << samples[i].col() << std::endl;
    }
}

答案 6 :(得分:1)

首先,你是以错误的顺序游行阵列!

您始终总是想要线性扫描内存。这意味着您的阵列的最后一个索引需要更快地更改。所以不要这样:

for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
        float sample = arr[i][j];

试试这个:

for (int i = 0; i < cols; i++) {
    for (int j = 0; j < rows; j++) {
        float sample = arr[i][j];

我预测这将比任何其他单一变化产生更大的差异。

接下来,我将使用堆而不是排序数组。标准<algorithm>标头已具有push_heappop_heap函数,可将向量用作堆。 (但是,除非M相当大,否则这可能无济于事。对于小M和随机数组,你最终不会做那么多次插入...像O(log N)我相信。)

接下来是使用SSE2。但这是花生,而不是以正确的顺序游历记忆。

答案 7 :(得分:0)

您应该能够通过并行处理获得近乎线性的加速。

使用N个CPU,您可以为每个CPU处理一个rows/N行(以及所有列),找到每个频段中的前M个条目。然后进行选择排序以找到整体顶部M

你也可以用SIMD做到这一点(但是在这里你要通过交错列来划分任务,而不是划分行)。不要试图让SIMD更快地进行插入排序,让它一次进行更多的插入排序,最后使用一个非常快的步骤进行组合。

当然你可以同时进行多线程和SIMD,但是在一个只有30x30的问题上,这不太可能是值得的。

答案 8 :(得分:0)

我尝试用float替换double,有趣的是,这让我的速度提升了大约20%(使用VC ++ 2008)。这有点违反直觉,但似乎现代处理器或编译器都针对双值处理进行了优化。

答案 9 :(得分:-1)

使用链接列表存储最佳的M值。您仍然需要迭代它以找到正确的位置,但插入是O(1)。它甚至可能比二进制搜索和插入O(N)+ O(1)vs O(lg(n))+ O(N)更好。 交换fors,因此您不会访问内存中的每个N元素并丢弃缓存。


LE:投入另一个可能适用于均匀分布值的想法 在3/2 * O(N ^ 2)比较中找出最小值,最大值 创建从N到N ^ 2均匀分布的桶中的任何地方,优选地比N更接近N ^ 2 对于NxN矩阵中的每个元素,将其放在bucket [(int)(value-min)/ range]中,range = max-min。
最后创建一个从最高桶到最低桶的集合,在| current set |时将其他桶中的元素添加到其中+ |下一个桶| &LT; = M
。 如果你得到了M元素,你就完成了。 你可能会得到比M更少的元素,让我们说P.
将算法应用于剩余的桶并从中获取最大的M-P元素 如果元素是统一的并且您使用N ^ 2个桶,那么它的复杂度大约是3.5 *(N ^ 2),而当前解是大约O(N ^ 2)* ln(M)。