我使用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
答案 0 :(得分:13)
在这个答案中,我提出了三个优化:
用std::set
替换对象boost::container::flat_set
以改善局部性(以及可能的重新分配成本,因为大多数对象集都是&lt; 4个元素)
更新在下面的最终版本中,只需将
boost::container::flat_map
替换为std::set
,仅将find_range
的性能降低2倍〜{ {1}}我的测试系统上的因子约为4倍
将对象ID find_range_ex
替换为std::string
(技术上是string_atom
,但在逻辑上是唯一的)。此技术类似于各种VM实现(如Java / .NET)中的实习字符串。
注意:
char const*
的当前实现非常简单,永远不会释放原子。您可能希望在双端队列中备份字符串,使用Boost Flyweights,池分配器或其中某些组合以使其更智能。但是,是否需要这取决于您的用例
用make_atom
调用替换地图交集,通过避免复制(大量)数据来节省大量时间
_ 更新当单独应用 此优化时,加速已经 26~30x 。请注意,与包含所有三个优化的情况相比,内存使用量大约是20MiB的两倍_
在开始之前,我想知道数据是什么样的。因此,编写一些代码来解析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)
您可以使用此代码来解决所有问题。
内存使用情况。
使用66425范围,您可以使用L1缓存,并且使用随附的字符串集也可以使用L2D,甚至可能超过L3。这意味着每次数据访问通常会有50-200个cpu周期的延迟,而乱序执行只会覆盖~32个周期,这意味着CPU基本上会一直停止。如果通过硬件预取程序顺序访问所有内存,则可以大大减轻这种情况。
指针追逐地图。
映射和集通常实现为带指针的rb_tree。 (interval_map可能不同?)。为了进一步增加问题,指针的访问将确保数据不会被顺序访问,这意味着您将受到高延迟的影响。
调用函数指针/虚函数。
令人惊讶的是,除非在f
内使用更多的区间函数,否则它不会显示在前12位。稍后当你解决了其他问题时,你可能会发现每次调用此函数都会为每次调用引入X个周期的延迟,其中X是CPU管道的长度。
如果您使用perf获取效果数据,请使用perf stat -d
添加运行结果。这应该显示上面提到的大量缓存未命中和空闲CPU的问题。
set<string>
的用法很糟糕,因为它的指针在追逐,你应该使用vector<string>
,你需要自己对它进行排序。这样可以加快f
中的访问速度,但不会减轻其他问题。
向interval_map
添加分配器,实现竞技场可能会加快访问速度,因为数据应该有更好的本地化。