一尘不染

std :: lower_bound的std :: vector比std :: map :: find慢

algorithm

我编写了一个类来充当顺序容器(std::vector/ std::queue/
std::list)的包装,以具有a的接口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,并使用MSVC10对POD从4到128字节(范围从8到2000个元素)进行了测试。

我期望在(1)从一定范围内为小对象创建,(2)对少量小对象进行随机插入/擦除以及(3)对所有对象进行查找方面具有更高的性能。出乎意料的是,根据在所有测试范围内创建的向量,向量
显着
更快,而对于最大约2048个字节(512个4字节对象或128个16字节对象等)的大小的随机擦除,向量的创建速度则更快。但是,最令人震惊的是,std::vector使用std::lower_bound速度比std::map::find所有POD
都要慢。对于4字节和8字节的POD,差异很小,但是对于128字节的POD,差异std::vector要慢36%!但是,对于std::stringstd::vector平均速度要快6%。

我感觉由于更好的缓存局部性/较小的内存大小,std::lower_bound排序后的std::vector表现应该会跑赢大盘std::map,而且由于map可能无法完美平衡,或者在
最坏的情况下 它应该 匹配
std::map,但我一生都无法想到std::map应该更快。我唯一的想法是谓词会以某种方式降低它的速度,但是我不知道怎么做。所以,问题是:
这怎么可能是std::lower_bound在一个排序std::vector可以通过跑赢std::map(在MSVC10)?

[编辑]我已经证实,std::lower_boundstd::vector<std::pair<4BYTEPOD,4BYTEPOD>>使用较少的
比较 平均比std::map<4BYTEPOD,4BYTEPOD>::find(由0-0.25),但我实现仍然高达26%速度较慢。

[POST-ANSWER-
EDIT]我在http://ideone.com/41iKt上创建了SSCCE,该SSCCE
删除了所有不需要的绒毛,并清楚地表明,find排序后的绒毛vector比慢map,约15%。


阅读 288

收藏
2020-07-28

共1个答案

一尘不染

这是一个更有趣的螺母!在到目前为止讨论我的发现之前,让我指出该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>with
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>就是对象的情况下,谓词将无法工作。

2020-07-28