如何优化向量的二进制搜索?

Posted

技术标签:

【中文标题】如何优化向量的二进制搜索?【英文标题】:How to optimize binary search of a vector? 【发布时间】:2014-05-29 15:48:46 【问题描述】:

我正在尝试在键值对的排序向量上实现查找方法。现在它的执行速度比 map.find(key) 慢。理论上它应该更快,因为向量可以更好地利用 CPU 缓存,因为它的内存是连续的。我只是想知道这个实现是否有任何明显的问题,是否有任何方法可以优化它?我不认为在这里使用标准算法是一个选项,因为最接近的可能选项是 lower_bound ,这将导致我必须执行检查以验证它是否找到任何东西的额外开销。除此之外,lower_bound 将需要我构建一个 pair(加上我在它周围放置的包装器)以将其作为我正在搜索的值,从而产生更多不必要的开销。

FlatMap<KEY, VALUE, COMPARATOR>::findImp(const key_type &key)

    typename VectorType::iterator lower = d_elements.begin();
    typename VectorType::iterator upper = d_elements.end();
    typename VectorType::iterator middle;
    while(lower < upper) 
        middle = lower + (upper-lower)/2;
        if(d_comparator(middle->data().first, key))
            lower = middle;
            ++lower;
         else if(d_comparator(key, middle->data().first))
            upper = middle;
         else 
            return middle;
        
    
    return d_elements.end();

请注意,d_elements 是对的向量(对在包装器中):

vector<FlatMap_Element<KEY, VALUE> >  d_elements;

包装器本身只是将这对作为数据成员保存,并通过不应该影响搜索的赋值来做一些魔术:

template <class KEY, class VALUE>
class FlatMap_Element 
    bsl::pair<const KEY, VALUE> d_data;
    ...
    pair<const KEY, VALUE>& data();
    pair<const KEY, VALUE> const& data() const;
;

我知道使用包装器的业务不是减速的原因,因为我已经在没有包装器的向量或对上测试了该算法并且具有相同的性能。

感谢任何调整建议。

【问题讨论】:

一个明显的改进是只使用一次 d_comparator。从理论上讲,您调用某个给定两个值返回 -1、0 或 1 的函数,并使用该信息重新分配您的界限。显然,您的比较器的工作方式不同,因为如果一个参数大于另一个参数,它返回true,否则返回false。我会考虑切换到只能调用一次的比较器,看看它是否有什么不同。 您拒绝 std::lower_bound 的最终相等性测试,而您的版本在每次迭代时都进行检查... 使用middle = (lower+upper)/2; 而不是middle = lower + (upper-lower)/2; 可能会减少一点算术。 “下限在每次迭代时都会进行完全相同的检查”。如果您已通过查看源代码验证了这一点,consider switching to an implementation that does it right。 不要假设没有基准测试的 lower_bound 太慢。事实上,如果没有基准测试/分析,不要假设任何事情。每当您开始猜测性能时,很有可能会出错。 【参考方案1】:

您的版本使用两次 m_comparator 循环结果,而 std::lower_bound 仅使用一次比较。

所以你可以使用类似:(C++03)

template <typename Key, typename Value, typename KeyComparator>
struct helper_comp

    bool operator (const std::pair<const Key, Value>& lhs, const Key& rhs) const 
        return comp(lhs.first, rhs);
    
    KeyComparator comp;
;

template <typename Key, typename Value, typename KeyComparator>
typename std::vector<std::pair<const Key, Value>>::const_iterator
my_find(const std::vector<std::pair<const Key, Value>>& v, const Key& key)

    auto it = std::lower_bound(v.begin(), v.end(), key, helper_comp<Key, Value, KeyComparator>());
    if (it != v.end() && it->first == key) 
        return it;
    
    return v.end();

或使用 lambda 代替外部 struct helper_comp(C++11) (https://ideone.com/snZTRu)

【讨论】:

helper_comp 不需要能够同时处理对/密钥和密钥/对比较的条件吗? 不确定,我认为 msvc 在调试时需要它来检查容器是否已排序。 这将是一对/一对比较。我认为为了安全起见,您应该提供所有 3 个,即使您的特定编译器实现没有使用所有这些。【参考方案2】:

我会在汇编语言级别单步执行它。 每条指令都应该发挥作用。 如果它看起来太复杂,那就是性能问题。

请记住 Jon Bentley 多年前给出的二分搜索示例。 如果表有 1024 个条目,它看起来像这样:

i = 0;
if (v >= a[i+512]) i += 512;
if (v >= a[i+256]) i += 256;
if (v >= a[i+128]) i += 128;
if (v >= a[i+ 64]) i +=  64;
if (v >= a[i+ 32]) i +=  32;
if (v >= a[i+ 16]) i +=  16;
if (v >= a[i+  8]) i +=   8;
if (v >= a[i+  4]) i +=   4;
if (v >= a[i+  2]) i +=   2;
if (v >= a[i+  1]) i +=   1;

Big-O 不是万能的。这仍然只是 O(log n),但它围绕着幼稚的实现运行。

【讨论】:

当我尝试它时,似乎并没有绕很多圈子。请记住,二分搜索的大部分时间都花在等待数据从内存中传回,而不是让 CPU 做一些有成效的事情。 @tmyklebu:您是否在汇编语言级别单步执行它,每行应该输出大约 4-5 条指令?好的,由于内存延迟严重,它可能会更慢。 你为什么在这里谈论计数指令?这个技巧会生成一段很好的代码。但是那段漂亮的代码并没有比循环版本快得多,因为执行时间主要是缓存未命中,而不是指令执行。 (实际上每行大约三个指令。) “多年前”,当指令和内存速度更加均衡时,这可能是值得的。虽然不是非常有用,因为您需要恒定数量的元素,即 2 的幂。【参考方案3】:

您可以尝试使用三元或四元搜索。第一批迭代本质上是进行随机内存访问。这有相当大的延迟。您可以在该延迟中隐藏更多的随机内存访问,并减少它们的执行次数。

这里的一个潜在缺陷是缓存关联性可能会使步幅为 2 的幂的高阶搜索表现不佳。

我还要补充一点,您额外的比较器调用确实对您没有多大帮助。你很幸运(在最后一次迭代之前找到了你正在寻找的东西)不到一半的时间。如果你修复了你的二分搜索,你只需要在最后一次迭代时检查它。

【讨论】:

以上是关于如何优化向量的二进制搜索?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用二分搜索将元素插入已排序的向量中

如何在 C++ 中的矩阵中搜索向量以及哪种算法?

使用向量的二分搜索

对向量使用二分搜索。

涉及向量的二分搜索问题 (C++)

如何将表示二进制的 int 向量转换为表示十进制数的 int 向量? C++