【问题标题】:More efficient way to populate unordered_set?填充 unordered_set 的更有效方法?
【发布时间】:2019-09-03 06:41:12
【问题描述】:

我有一个整数数组连续存储在内存中,我想将它们全部添加到 unordered_set 集合中。

现在,我一次添加一个。

for (int i = 0; i < count; i++)
    collection.insert(pi[i]);

有什么方法可以更有效地做到这一点?

我意识到项目并没有连续存储在集合中,因此它不会像将数组交给集合那样简单。但这可以通过某种方式进行优化吗?

【问题讨论】:

    标签: c++ visual-c++ stl unordered-set


    【解决方案1】:

    unordered_set 有一个构造函数,它接受一系列元素来初始添加它们:

    template< class InputIt >
    unordered_set( InputIt first, InputIt last,
               size_type bucket_count = /*implementation-defined*/,
               const Hash& hash = Hash(),
               const key_equal& equal = key_equal(),
               const Allocator& alloc = Allocator() );
    

    所以你可以只做collection = std::unordered_set{ p, p + count }; 并留给实现。

    正如其他用户在 cmets 中指出的那样,insert 也有一个重载,需要一个范围:

    template< class InputIt >
    void insert( InputIt first, InputIt last );
    

    所以,就像调用构造函数一样,你可以这样做,collection.insert(p, p + count);

    不能保证这种重载会更有效,因为平均而言,这两种重载的复杂性是线性的,并且只是一个接一个地插入元素。

    其实,如果我们看看insert在MSVC中是如何实现的,其实很简单

    template<class _Iter>
    void insert(_Iter _First, _Iter _Last)
    {   // insert [_First, _Last) at front, then put in place
        _DEBUG_RANGE(_First, _Last);
        for (; _First != _Last; ++_First)
            emplace(*_First);
    }
    

    所以没有针对这种情况进行优化。

    我认为,解决此问题的最佳方法是调用reserve,如果您知道要添加的元素数量,并且如果有很多冲突(整数不会出现冲突),可能会修改bucket_count

    【讨论】:

    • 是否有证据表明这些替代方案更有效?如果有,证据是什么,标准是什么?
    • @Yakk-AdamNevraumont:我的印象是实施取决于实施者。但是,这两种方法中的任何一种都可以让您的代码受益于此类的未来版本中所做的优化。
    • @Yakk-AdamNevraumont 显然,就需要编写的代码量而言,它们效率更高......
    • 我已经添加了我对运行时效率的想法。我认为reservebucket_count 是您对添加元素效率的唯一合理控制。
    • @JonathanWood 相当幽默... 可能,函数调用inside循环(单元素插入)可能 与迭代器变体一起被移出,但是否有利润完全取决于实现(单元素变体是否内联,是否在迭代器变体中进一步调用函数)...
    【解决方案2】:

    使用基于范围的构造函数或插入方法将简洁而优雅,但可能与您的方法一样有效。 原因是传递给这些函数的迭代器是输入迭代器,而不是随机迭代器。 因此,无法计算范围的长度,并且当集合的负载因子变高时,必须通过周期性的重新散列一个一个地插入元素。

    考虑调用 std::unordered_set 的reserve 方法。

    collection.reserve(pi.size());
    collection.insert(pi.begin(), pi.end());
    

    编辑: 正如 cmets 中所提到的,人们还可能会担心一个一个地对插入的元素进行哈希处理的效率。 然后,能够执行某种批量插入将是有效的。 但是,在 OP 的情况下,元素是整数,在 std::hash 的大多数(如果不是所有)实现中使用恒等函数进行散列,这不会花费那么多;)。事实上,它是随机整数所能得到的最好的散列函数。其他散列函数可能更适合“有组织”的集合。

    编辑2: 评论部分现在正在推测插入方法的更好实现。 我认为基于范围的插入重载要求输入迭代器,所以是的,您实际上可以传递任何非输出迭代器。 还要看一下范围插入的最坏情况复杂性:您会看到它被指定为允许一个一个地插入元素。 最后,看一下 insert 方法的一些实现,您会发现随机访问迭代器没有特别的重载。 这是有道理的,因为没有理由在 insert 方法中强加额外的检查,而对于我们希望将容器设置为至少给定容量的情况,reserve 方法在这里。 基于此,上面的答案很可能是基于标准库实际实现的最佳技术。

    【讨论】:

    • 除了重新分配问题之外,还有另一个(可能更相关)我们无法提高效率的原因:每个元素都需要独立与其他元素进行散列,有无法从要插入的元素的任何关系中获利(例如,可以节省一些哈希计算或在垃圾中插入元素组)。
    • @Aconcagua,你能做些什么呢?如果需要使用 std::unordered_set,那么我想不出更好的方法来插入给定数量的元素。
    • 原因是传递给这些函数的迭代器是输入迭代器而不是随机迭代器 ==>我实际上会说构造函数采用范围需要(至少)输入迭代器
    • 您仍然可以将 random-access iterators 传递给该构造函数,因为这些迭代器也满足 input iterators 的要求(即随机访问迭代器也是一个输入迭代器)。请注意,数组提供的迭代器是随机访问迭代器。因此,如果传递随机访问迭代器而不是严格输入迭代器,则特定实现可能能够通过计算将要插入的元素的数量并相应地保留避免多次重新散列所需的空间来利用这一点。
    • (详述@ElProfesor 评论)在 C++ 中模板是透明的:类型参数是完全可用的。即使在带有概念的 C++ 中(甚至不是适当的元类型系统)。如果在不需要这种“更丰富”类型的模板中使用支持许多操作的类型,则该类型有可用的操作,即使模板预计在没有这些操作的情况下也可以工作。代码可以在可用时使用它们。这与 O'Caml 模块实例化不同,后者会擦除未列出的任何操作。这使得模板比更干净的 O'Caml 更强大,但也更像宏。
    猜你喜欢
    • 1970-01-01
    • 2012-04-01
    • 2021-06-03
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多