std :: lower_bound对于std :: vector比std :: map :: find慢

时间:2012-01-09 06:38:38

标签: c++ performance algorithm vector map

我编写了一个类来充当顺序容器(std::vector / std::queue / std::list)的包装器,以获得std::map的接口,以提高性能。使用少量的小物件。鉴于已有的算法,编码非常简单。这段代码显然是从我的完整代码高度修剪,但显示了问题。

template <class key_, 
          class mapped_, 
          class traits_ = std::less<key_>,
          class undertype_ = std::vector<std::pair<key_,mapped_> >
         >
class associative
{
public:
    typedef traits_ key_compare;
    typedef key_ key_type;
    typedef mapped_ mapped_type;
    typedef std::pair<const key_type, mapped_type> value_type;
    typedef typename undertype_::allocator_type allocator_type;
    typedef typename allocator_type::template rebind<value_type>::other value_allocator_type;
    typedef typename undertype_::const_iterator const_iterator;

    class value_compare {
        key_compare pred_;
    public:
        inline value_compare(key_compare pred=key_compare()) : pred_(pred) {}
        inline bool operator()(const value_type& left, const value_type& right) const {return pred_(left.first,right.first);}
        inline bool operator()(const value_type& left, const key_type& right) const {return pred_(left.first,right);}
        inline bool operator()(const key_type& left, const value_type& right) const {return pred_(left,right.first);}
        inline bool operator()(const key_type& left, const key_type& right) const {return pred_(left,right);}
        inline key_compare key_comp( ) const {return pred_;}
    };
    class iterator  {
    public:       
        typedef typename value_allocator_type::difference_type difference_type;
        typedef typename value_allocator_type::value_type value_type;
        typedef typename value_allocator_type::reference reference;
        typedef typename value_allocator_type::pointer pointer;
        typedef std::bidirectional_iterator_tag iterator_category;
        inline iterator(const typename undertype_::iterator& rhs) : data(rhs) {}
    inline reference operator*() const { return reinterpret_cast<reference>(*data);}
        inline pointer operator->() const {return reinterpret_cast<pointer>(structure_dereference_operator(data));}
        operator const_iterator&() const {return data;}
    protected:
        typename undertype_::iterator data;
    };

    template<class input_iterator>
    inline associative(input_iterator first, input_iterator last) : internal_(first, last), comp_() 
    {if (std::is_sorted(internal_.begin(), internal_.end())==false) std::sort(internal_.begin(), internal_.end(), comp_);}

inline iterator find(const key_type& key) {
    iterator i = std::lower_bound(internal_.begin(), internal_.end(), key, comp_);
    return (comp_(key,*i) ? internal_.end() : i);
}

protected:
    undertype_ internal_;
    value_compare comp_;
};

SSCCE位于http://ideone.com/Ufn7r,完整代码位于http://ideone.com/MQr0Z(注意:IdeOne产生的时间非常不稳定,可能是由于服务器负载,并且没有明确显示有问题的结果)

我测试了std::string,POD从4到128字节,范围从8到2000个元素与MSVC10。

我期望(1)从小对象的范围创建,(2)少量小对象的随机插入/擦除,以及(3)查找所有对象的更高性能。令人惊讶的是,从所有测试的范围创建的向量显着更快,随机擦除的速度更快,取决于大约2048字节的大小(512个4字节对象,或128个16字节对象,等等)。然而,最令人震惊的是,使用std::vector的{​​{1}}比所有POD的std::lower_bound慢。对于4字节和8字节POD,差异微乎其微,但对于128字节POD,std::map::find速度降低了36%!但是,对于std::vectorstd::string的平均速度提高了6%。

由于更好​​的缓存位置/更小的内存大小,我觉得排序std::vector上的std::lower_bound应该优于std::vector,并且由于std::map可能不完美平衡,或者在最坏的情况下它应匹配 map,但在我的生活中不能想到std::map应该更快的任何理由。我唯一的想法是谓词在某种程度上减慢了它,但我无法弄清楚如何。所以问题是:排序std::map的{​​{1}}如何能够超越std::lower_bound(在MSVC10中)?

[编辑]我已确认std::vector std::map平均使用比较的次数少于std::lower_bound(0-0.25),但我的实施仍然慢了26%。

[POST-ANSWER-EDIT]我在http://ideone.com/41iKt制作了一个SSCCE,删除了所有不需要的绒毛,并清楚地表明排序std::vector<std::pair<4BYTEPOD,4BYTEPOD>>上的std::map<4BYTEPOD,4BYTEPOD>::findfindvector {1}},约15%。

2 个答案:

答案 0 :(得分:10)

这是一个更有趣的坚果要破解!在讨论我到目前为止的调查结果之前,我要指出associative::find()函数与std::map::find()的行为不同:如果未找到密钥,则前者返回下限,而后者返回end() 。要解决此问题,需要将associative::find()更改为以下内容:

auto rc = std::lower_bound(this->internal_.begin(), this->internal_.end(), key, this->comp_);
return rc != this->internal_.end() && !this->comp_(key, rc->first)? rc: this->internal_.end();

现在我们更有可能比较苹果和苹果(我还没有验证逻辑是否真的正确),让我们继续调查性能。我不太相信用于测试性能的方法确实存在水,但我现在坚持使用它,我绝对可以提高associative容器的性能。我不认为我在代码中发现了所有性能问题,但至少取得了一些进展。最大的问题是注意到associative中使用的比较函数非常糟糕,因为它一直在制作副本。这使得这个容器处于劣势。如果您现在正在检查比较器,您可能看不到它,因为看起来好像这个比较器 通过引用传递!这个问题实际上相当微妙:底层容器的value_typestd::pair<key_type, mapped_type>,但比较器以std::pair<key_type const, mapped_type>为参数!解决这个问题似乎给关联容器带来了相当大的性能提升。

要实现一个比较器类,它没有机会完全匹配参数,我正在使用一个简单的帮助器来检测类型是否为std::pair<L, R>

template <typename>               struct is_pair                  { enum { value = false }; };
template <typename F, typename S> struct is_pair<std::pair<F, S>> { enum { value = true }; };

...然后我用这个替换比较器,稍微复杂一点:

class value_compare {
    key_compare pred_;
public:
    inline value_compare(key_compare pred=key_compare()) : pred_(pred) {}
    template <typename L, typename R>
    inline typename std::enable_if<is_pair<L>::value && is_pair<R>::value, bool>::type
    operator()(L const& left, R const& right) const {
        return pred_(left.first,right.first);
    }
    template <typename L, typename R>
    inline typename std::enable_if<is_pair<L>::value && !is_pair<R>::value, bool>::type
    operator()(L const& left, R const& right) const {
        return pred_(left.first,right);
    }
    template <typename L, typename R>
    inline typename std::enable_if<!is_pair<L>::value && is_pair<R>::value, bool>::type
    operator()(L const& left, R const& right) const {
        return pred_(left,right.first);
    }
    template <typename L, typename R>
    inline typename std::enable_if<!is_pair<L>::value && !is_pair<R>::value, bool>::type
    operator()(L const& left, R const& right) const {
        return pred_(left,right);
    }
    inline key_compare key_comp( ) const {return pred_;}
};

这通常使两种方法更加接近。鉴于我认为std::vector<T> lower_bound()方法应该比使用std::map<K, T>要好得多,我觉得调查尚未结束。

<强>附录

重新思考练习,我发现为什么我对谓词类的实现感到不舒服:复杂的方式!使用std::enable_if进行更改可以更简单地完成此操作:这可以很好地将代码简化为更容易阅读的内容。关键是得到钥匙:

template <typename Key>
Key const& get_key(Key const& value)                  { return value; }
template <typename Key,  typename Value>
Key const& get_key(std::pair<Key, Value> const& pair) { return pair.first; }

使用此实现从值或一对值中获取“键”,谓词对象可以只定义一个非常简单的函数调用操作符:

template <typename L, typename R>
bool operator()(L const& l, R const& r)
{
    return this->pred_(get_key<key_type>(l), get_key<key_type>(r));
}

虽然有一个小技巧,但是:需要将key_type传递给get_key()函数。如果没有这个,谓词就不适用于key_type本身就是std::pair<F, S>个对象的情况。

答案 1 :(得分:1)

我有一个猜测。首先,lower_bound 必须执行 log2(n)比较,无论如何。这意味着永远不会有时间(就像find那样)。其次,对于大于特定大小的数据类型,必须在向量的任何指针算术中涉及乘法运算。而对于地图,它只是指针从内存中加载4(或64位上的8)字节值。

x86有一些很好的指令,可以在索引计算过程中以2的幂进行非常快速的乘法运算。但是它们仅适用于2的小功率,因为​​它们被设计用于索引类似整数的实体的数组。对于较大的数字,它必须实际使用明显较慢的整数乘法指​​令。

执行lower_bound时,您必须完全执行这些乘法的 log2(n)。但是对于find,它可以用较小的数字切断一半的值。这意味着lower_bound对任何其他方法的影响要大得多。

另外......在我看来,::std::map应该实现为B树,其中每个节点都是一个页面大小。虚拟内存设置它,以便基本上每个具有大量数据结构的程序最终都会在内存压力下将该结构的一部分分页。让每个节点只存储一个值可能会产生一个几乎最糟糕的情况,你必须在整个页面中为每个 log2(n)深度的比较进行分页,如果你使用了一个b-tree,最差的分页情况是 logx(n)页面,其中 x 是每个节点的值数。

这也具有减轻缓存行边界的不良影响的良好副作用。将存在(键,值)元组大小和高速缓存行大小的LCM。在节点中具有多个(键,值)对将其设置为使得该LCM更可能发生并且X对将精确地采用Y个高速缓存行。但是,如果每个节点只包含一对,那么除非节点大小是缓存行大小的精确倍数,否则基本上不会发生这种情况。