std::map 和性能,相交集

Posted

技术标签:

【中文标题】std::map 和性能,相交集【英文标题】:std::map and performance, intersecting sets 【发布时间】:2009-06-29 01:39:13 【问题描述】:

我正在交叉一些数字集,并通过存储每次在地图中看到数字时的计数来做到这一点。

我发现性能非常缓慢。

详情: - 其中一组有 150,000 个数字 - 该组和另一组的交集第一次大约需要300ms,第二次大约需要5000ms - 我还没有进行任何分析,但是每次我在 malloc.c 中进行交集时都会中断调试器!

那么,我怎样才能提高这种性能呢?切换到不同的数据结构?一些如何提高map的内存分配性能?

更新:

    有什么方法可以询问 std::map 或 boost::unordered_map 预分配 一些空间? 或者,有什么技巧可以有效地使用这些技巧吗?

更新2:

见Fast C++ container like the C# HashSet<T> and Dictionary<K,V>?

更新3:

我对 set_intersection 进行了基准测试,结果很糟糕:

(set_intersection) Found 313 values in the intersection, in 11345ms
(set_intersection) Found 309 values in the intersection, in 12332ms

代码:

int runIntersectionTestAlgo()
   

    set<int> set1;
    set<int> set2;
    set<int> intersection;


    // Create 100,000 values for set1
    for ( int i = 0; i < 100000; i++ )
    
        int value = 1000000000 + i;
        set1.insert(value);
    

    // Create 1,000 values for set2
    for ( int i = 0; i < 1000; i++ )
    
        int random = rand() % 200000 + 1;
        random *= 10;

        int value = 1000000000 + random;
        set2.insert(value);
    

    set_intersection(set1.begin(),set1.end(), set2.begin(), set2.end(), inserter(intersection, intersection.end()));

    return intersection.size(); 

【问题讨论】:

我建议对其进行分析以收集更多信息。如果您要分配大型数据结构,则 malloc 是一种可能性。 如果我在地图中注释掉插入和查找,然后在我的集合中旋转,时间会大大缩短。 90% 的时间都花在了地图上。我尝试切换到 boost::unordered_map,结果相同。 嗨,Alex,你的最终版本是什么?还在使用 std::set 吗?还是排序向量? [出于兴趣]谢谢。 【参考方案1】:

您绝对应该使用更快的预分配向量。与 stl 集合进行集合交集的问题在于,每次移动到下一个元素时,您都在追逐一个动态分配的指针,而该指针很容易不在您的 CPU 缓存中。使用向量,下一个元素通常会在您的缓存中,因为它在物理上靠近前一个元素。

向量的诀窍在于,如果您不为这样的任务预先分配内存,它会执行更糟糕的操作,因为它会在初始化步骤中调整自身大小时继续重新分配内存。

试试这样的方法 - 它会更快。

int runIntersectionTestAlgo()  

vector<char> vector1; vector1.reserve(100000);
vector<char> vector2; vector2.reserve(1000);

// Create 100,000 values for set1
for ( int i = 0; i < 100000; i++ )    
    int value = 1000000000 + i;
    set1.push_back(value);


sort(vector1.begin(), vector1.end());

// Create 1,000 values for set2
for ( int i = 0; i < 1000; i++ )    
    int random = rand() % 200000 + 1;
    random *= 10;
    int value = 1000000000 + random;
    set2.push_back(value);


sort(vector2.begin(), vector2.end());

// Reserve at most 1,000 spots for the intersection
vector<char> intersection; intersection.reserve(min(vector1.size(),vector2.size()));
set_intersection(vector1.begin(), vector1.end(),vector2.begin(), vector2.end(),back_inserter(intersection));

return intersection.size(); 

【讨论】:

嗨,克里斯,这与我的最新代码有何不同?最新的代码确实在向量中保留空间,确实将向量传递给 set_intersection 等。 @Chris:对不起,看到这个帖子:***.com/questions/1060648/… 它使用你描述的方法,而且肯定更快。【参考方案2】:

在不了解您的问题的情况下,“用一个好的分析器检查”是我能给出的最好的一般建议。除此之外……

如果内存分配是您的问题,请切换到某种池分配器,以减少对malloc 的调用。 Boost 有许多应该与std::allocator&lt;T&gt; 兼容的自定义分配器。事实上,如果您已经注意到调试中断示例总是以malloc 结尾,您甚至可以在分析之前尝试此操作。

如果您的数字空间已知是密集的,您可以切换到使用基于 vector- 或 bitset 的实现,使用您的数字作为向量中的索引。

如果您的数字空间大多是稀疏的,但有一些自然聚类(这是一个很大的if),您可以切换到向量图。使用高阶位进行地图索引,使用低阶位进行向量索引。这在功能上与简单地使用池分配器非常相似,但它可能会为您提供更好的缓存行为。这是有道理的,因为您正在向机器提供更多信息(集群是显式且缓存友好的,而不是您期望从池分配中获得的随机分布)。

【讨论】:

【参考方案3】:

我会支持对它们进行排序的建议。已经有 STL 集合算法对排序范围进行操作(如 set_intersection、set_union 等):

set_intersection

【讨论】:

我可能会尝试这样做,但这需要我先完整获取每个集合,然后对它们进行排序。我已经在 C# 中使用 HashSet 实现了同样的算法,而且速度非常快。 C# 算法也针对相同的数据运行。 C# 内存分配更快吗?还是 HashSet 比 map/hash_map/unordered_map 快?【参考方案4】:

我不明白为什么你必须使用地图来做路口。就像人们说的那样,您可以将集合放入std::set's,然后使用std::set_intersection()

或者您可以将它们放入hash_set's。但是你必须手动实现交集:从技术上讲,你只需要将其中一组放入hash_set,然后循环遍历另一组,并测试每个元素是否包含在hash_set中。

【讨论】:

【参考方案5】:

与地图的交叉点很慢,试试hash_map。 (但是,并非所有 STL 实现都提供此功能。

或者,对地图进行排序并以类似合并排序的方式进行。

【讨论】:

我试过hash_map和boost::unordered_map,同样的性能很差。【参考方案6】:

你的交集算法是什么?也许有一些改进?

这是另一种方法

我不知道它是更快还是更慢,但可以尝试一下。在这样做之前,我还建议使用分析器来确保您确实在热点上工作。将相交的数字组更改为使用std::set&lt;int&gt;。然后遍历最小的一个,查看您找到的每个值。对于最小集合中的每个值,使用find 方法查看该数字是否存在于其他每个集合中(为了提高性能,从最小到最大搜索)。

这是在没有在所有集合中找到数字的情况下优化的,所以如果交集比较小,可能会很快。

然后,将交集存储在 std::vector&lt;int&gt; 中 - 使用 push_back 插入也非常快。

这是另一种替代方法

将数字集更改为std::vector&lt;int&gt; 并使用std::sort 从小到大排序。 然后使用std::binary_search 查找值,使用与上述大致相同的方法。这可能比搜索std::set 更快,因为该数组在内存中更紧密。 实际上,没关系,您可以锁定步长遍历值,查看具有相同的值价值。仅增加小于您在上一步中看到的最小值的迭代器(如果值不同)。

【讨论】:

我有 N 个迭代器,每个集合一个。每次通过一个循环,我都会推进每个迭代器,增加我看到每个值的次数,一旦一个值的计数达到 N,那么我就找到了一个存在于所有集合中的值。我正在使用 std:map 来跟踪每个值的计数。 第二件事,这正是 std::set_intersection 已经做的。对于你的第一件事,它需要像 O(m*log(n)) 如果你已经有 std::sets (排序容器)效率低下,所以你可以在它们上使用 std::set_intersection ,其中 O(m +n)。 将 set_intersection 与实际的 std::set 一起使用可能很糟糕。我会尝试改用排序向量 你还在插入一个集合吗?还是向量?我认为您想插入向量 - 您可以在运行之前设置向量的大小,这样就不必重新分配向量的内存。您还在 malloc 中花费大部分时间吗?如果是这样,那么预分配要插入的向量会更快【参考方案7】:

可能是你的算法。据我了解,您正在旋转每组(我希望这是一个标准组),然后将它们扔到另一张地图中。这做了很多你不需要做的工作,因为标准集的键已经按排序顺序排列。相反,采取类似“合并排序”的方法。旋转每个迭代,取消引用以找到最小值。计算具有该最小值的数字,并增加这些数字。如果计数为 N,则将其添加到交集。重复直到第一张地图到达终点(如果您在开始之前比较大小,则不必每次都检查每张地图的终点)。

响应更新:确实存在通过预先保留空间来加速内存分配的功能,例如boost::pool_alloc。比如:

std::map<int, int, std::less<int>, boost::pool_allocator< std::pair<int const, int> > > m;

但老实说,malloc 非常擅长它的功能;在做任何过于极端的事情之前,我会先分析一下。

【讨论】:

这是个好主意...但是现在我的集合没有排序。我可能会先尝试对它们进行排序。 更多关于你正在交叉的这些“集合”的信息可能会有所帮助 - 它们是否像向量? 它们存储在内存数据库中,它们是数字列表,我希望不必将整个集合放在一个数据结构中并在开始相交之前对其进行排序。 我会尝试 boost::pool_alloc 看看它是否会有所作为。 我在我的 boost:unordered_map 上尝试了 boost::pool_alloc 和 boost::fast_pool_alloc 并且性能变慢了。【参考方案8】:

查看您的算法,然后选择正确的数据类型。如果您要进行类似集合的行为,并且想要进行交叉路口等,std::set 是要使用的容器。

由于它的元素以排序方式存储,插入可能会花费您 O(log N),但与另一个(排序!)std::set 的交集可以在线性时间内完成。

【讨论】:

【参考方案9】:

我想通了:如果我将调试器附加到 RELEASE 或 DEBUG 构建(例如在 IDE 中按 F5),那么我会遇到可怕的情况。

【讨论】:

以上是关于std::map 和性能,相交集的主要内容,如果未能解决你的问题,请参考以下文章

支持删除的不相交集数据结构

数据结构——不相交集

如何在 django 中相交两个查询集?

不相交集类

并查集(不相交集)的Union操作

Union/find--不相交集类(并查集)