如何提高boost interval_map查找的性能

时间:2014-11-26 15:24:27

标签: c++ boost

我使用boost::icl::interval_map将字节范围映射到一组字符串。地图从(已排序的)磁盘文件加载,然后使用下面的代码进行查找。

问题是查找速度很慢。

在我的测试中,我在地图中插入了66425个范围。我描述了代码,基本上> 99.9%的时间花在了各种Boost功能上(不是特定的功能很慢,很多时间分布在许多功能上)。

可以做些什么来加快速度?

我的树是否不平衡(我怎么知道?我怎样才能重新平衡它?)

正在使用set< string>一个问题?

计算地图和窗口的交点是否有问题? (虽然这是我需要的,所以我不知道怎么做)。

using namespace std;
typedef set<string> objects;
typedef boost::icl::interval_map<uint64_t, objects> ranges;
void
find_range (const ranges *map, uint64_t start, uint64_t end,
            void (*f) (uint64_t start, uint64_t end, const char *object,
                       void *opaque),
            void *opaque)
{
  boost::icl::interval<uint64_t>::type window;
  window = boost::icl::interval<uint64_t>::right_open (start, end);

  ranges r = *map & window;

  ranges::iterator iter = r.begin ();
  while (iter != r.end ()) {
    boost::icl::interval<uint64_t>::type range = iter->first;
    uint64_t start = range.lower ();
    uint64_t end = range.upper ();

    objects obj_set = iter->second;
    objects::iterator iter2 = obj_set.begin ();
    while (iter2 != obj_set.end ()) {
      f (start, end, iter2->c_str (), opaque);
      iter2++;
    }
    iter++;
  }
}

前几个个人资料条目:

  %   cumulative   self              self     total
 time   seconds   seconds    calls  us/call  us/call  name
  9.77      0.13     0.13 21866814     0.01           boost::icl::interval_bounds::interval_bounds(unsigned char)
  6.02      0.21     0.08  9132134     0.01           boost::icl::interval_traits<boost::icl::discrete_interval<unsigned long, std::less> >::lower(boost::icl::discrete_interval<unsigned long, std::less> const&)
  6.02      0.29     0.08  6004967     0.01           boost::enable_if<boost::icl::is_discrete_interval<boost::icl::discrete_interval<unsigned long, std::less> >, bool>::type boost::icl::is_empty<boost::icl::discrete_interval<unsigned long, std::less> >(boost::icl::discrete_interval<unsigned long, std::less> const&)
  5.26      0.36     0.07 21210093     0.00           boost::icl::discrete_interval<unsigned long, std::less>::bounds() const
  5.26      0.43     0.07 11964109     0.01           std::less<unsigned long>::operator()(unsigned long const&, unsigned long const&) const
  4.51      0.49     0.06 35761849     0.00           std::_Rb_tree<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::_Identity<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::_S_left(std::_Rb_tree_node_base const*)
  4.51      0.55     0.06 12009934     0.00           boost::icl::operator==(boost::icl::interval_bounds, boost::icl::interval_bounds)
  3.76      0.60     0.05 12078493     0.00           boost::icl::discrete_interval<unsigned long, std::less>::upper() const
  3.76      0.65     0.05 12077959     0.00           boost::enable_if<boost::icl::is_interval<boost::icl::discrete_interval<unsigned long, std::less> >, boost::icl::interval_traits<boost::icl::discrete_interval<unsigned long, std::less> >::domain_type>::type boost::icl::upper<boost::icl::discrete_interval<unsigned long, std::less> >(boost::icl::discrete_interval<unsigned long, std::less> const&)
  3.76      0.70     0.05  8837475     0.01           boost::icl::interval_bounds::bits() const
  3.76      0.75     0.05  6004967     0.01           boost::enable_if<boost::icl::is_interval<boost::icl::discrete_interval<unsigned long, std::less> >, bool>::type boost::icl::domain_less_equal<boost::icl::discrete_interval<unsigned long, std::less> >(boost::icl::interval_traits<boost::icl::discrete_interval<unsigned long, std::less> >::domain_type const&, boost::icl::interval_traits<boost::icl::discrete_interval<unsigned long, std::less> >::domain_type const&)
  3.01      0.79     0.04  5891650     0.01           boost::icl::is_right_closed(boost::icl::interval_bounds)

数据集:http://oirase.annexia.org/tmp/bmap.txt
完整代码:http://git.annexia.org/?p=virt-bmap.git;a=tree

2 个答案:

答案 0 :(得分:13)

在这个答案中,我提出了三个优化:

  1. std::set替换对象boost::container::flat_set以改善局部性(以及可能的重新分配成本,因为大多数对象集都是&lt; 4个元素)

      

    更新在下面的最终版本中,只需将boost::container::flat_map替换为std::set,仅将find_range的性能降低2倍〜{ {1}}我的测试系统上的因子约为4倍

  2. 将对象ID find_range_ex替换为std::string(技术上是string_atom,但在逻辑上是唯一的)。此技术类似于各种VM实现(如Java / .NET)中的实习字符串。

      

    注意char const*的当前实现非常简单,永远不会释放原子。您可能希望在双端队列中备份字符串,使用Boost Flyweights,池分配器或其中某些组合以使其更智能。但是,是否需要这取决于您的用例

  3. make_atom调用替换地图交集,通过避免复制(大量)数据来节省大量时间

      

    _ 更新当单独应用 此优化时,加速已经 26~30x 。请注意,与包含所有三个优化的情况相比,内存使用量大约是20MiB的两倍_

  4. 数量和数据效率

    在开始之前,我想知道数据是什么样的。因此,编写一些代码来解析equal_range输入,我们得到:

    <强> Source On Coliru

    bmap.txt

    正如您所看到的,这些集合通常是~3个项目,而很多是重复的。

    使用Parsed ok Histogram of 66425 input lines d: 3367 f: 20613 p: 21222 v: 21223 ranges size: 6442450944 ranges iterative size: 21223 Min object set: 1.000000 Max object set: 234.000000 Average object set: 3.129859 Min interval width: 1024.000000 Max interval width: 2526265344.000000 Average interval width: 296.445177k First: [0,1048576) Last: [3916185600,6442450944) String atoms: 23904 unique in 66425 total Atom efficiency: 35.986451% 对象命名方法,make_atom将内存分配从 ~15GiB 减少到 ~10Gib 。< / p>

    此优化还减少了字符串与集合插入的指针比较和boost::flat_set的组合策略的比较,因此对于较大的数据集,这可能会有很大的加速。

    查询效率

    输入的部分副本确实严重削弱了查询性能。

    不要复制,只需替换以下内容即可查看重叠范围:

    interval_map

      const ranges r = *map & window;
      ranges::const_iterator iter = r.begin ();
      while (iter != r.end ()) {
    

    在我的系统上运行10000个相同的随机查询,两个版本都会导致加速度为32x

      auto r = map->equal_range(window);
      ranges::const_iterator iter = r.first;
      while (iter != r.second) {
    

    运行时现在由解析/统计主导。完整的基准代码位于: On Coliru

    10000 'random' OLD lookups resulted in 156729884 callbacks in 29148ms
    10000 'random' NEW lookups resulted in 156729884 callbacks in 897ms
    
    real    0m31.715s
    user    0m31.664s
    sys 0m0.012s
    

答案 1 :(得分:1)

您可以使用此代码来解决所有问题。

  1. 内存使用情况。
    使用66425范围,您可以使用L1缓存,并且使用随附的字符串集也可以使用L2D,甚至可能超过L3。这意味着每次数据访问通常会有50-200个cpu周期的延迟,而乱序执行只会覆盖~32个周期,这意味着CPU基本上会一直停止。如果通过硬件预取程序顺序访问所有内存,则可以大大减轻这种情况。

  2. 指针追逐地图。
    映射和集通常实现为带指针的rb_tree。 (interval_map可能不同?)。为了进一步增加问题,指针的访问将确保数据不会被顺序访问,这意味着您将受到高延迟的影响。

  3. 调用函数指针/虚函数。 令人惊讶的是,除非在f内使用更多的区间函数,否则它不会显示在前12位。稍后当你解决了其他问题时,你可能会发现每次调用此函数都会为每次调用引入X个周期的延迟,其中X是CPU管道的长度。

  4. 如果您使用perf获取效果数据,请使用perf stat -d添加运行结果。这应该显示上面提到的大量缓存未命中和空闲CPU的问题。

    set<string>的用法很糟糕,因为它的指针在追逐,你应该使用vector<string>,你需要自己对它进行排序。这样可以加快f中的访问速度,但不会减轻其他问题。

    interval_map添加分配器,实现竞技场可能会加快访问速度,因为数据应该有更好的本地化。