【问题标题】:Why is unordered_map "find + insert" faster than "insert + check for success"?为什么unordered_map“查找+插入”比“插入+检查成功”快?
【发布时间】:2015-08-04 08:13:22
【问题描述】:

我使用 unordered_map 作为稀疏 3D 数组 (128 x 128 x 128) 将值插入到网格中,前提是网格单元仍然空闲。

到目前为止,我总是使用 find() 检查单元格是否空闲,如果是,那么我使用 insert() 或 emplace() 添加了一个元素。 现在我发现我可以使用 insert 和 emplace 的返回值来检查元素是否已添加,或者地图中是否已经存在具有相同键的元素。我认为这可以提高性能,因为我可以完全删除 find 的使用。

事实证明,不是通过插入而不查找来提高性能,而是性能实际上下降了,我不知道为什么。

我已将我的应用程序简化为这个示例,其中点是随机生成的,然后插入到网格中。

#include <unordered_map>
#include <random>
#include <chrono>
#include <iostream>
#include <math.h>
#include <algorithm>
#include <string>

using std::cout;
using std::endl;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
using std::chrono::duration_cast;
using std::unordered_map;

int num_elements = 5'000'000;


void findThenInsert(){
    cout << endl << "find and emplace" << endl;

    auto start = high_resolution_clock::now();

    std::mt19937 gen(123);
    std::uniform_real_distribution<> dis(0, 128);

    unordered_map<int, int> grid;
    int count = 0;
    for(int i = 0; i < num_elements; i++){
        float x = dis(gen);
        float y = dis(gen);
        float z = (cos(x*0.1) * sin(x*0.1) + 1.0) * 64.0;

        int index = int(x) + int(y) * 128 + int(z) * 128 * 128;
        auto it = grid.find(index);
        if(it == grid.end()){
            grid.emplace(index, count);
            count++;
        }
    }

    cout << "elements: " << count << endl;
    cout << "load factor: " << grid.load_factor() << endl;

    auto end = high_resolution_clock::now();
    long long duration = duration_cast<milliseconds>(end - start).count();
    float seconds = duration / 1000.0f;
    cout << seconds << "s" << endl;
}


void insertThenCheckForSuccess(){
    cout << endl << "emplace and check success" << endl;

    auto start = high_resolution_clock::now();

    std::mt19937 gen(123);
    std::uniform_real_distribution<> dis(0, 128);

    unordered_map<int, int> grid;
    int count = 0;
    for(int i = 0; i < num_elements; i++){
        float x = dis(gen);
        float y = dis(gen);
        float z = (cos(x*0.1) * sin(x*0.1) + 1.0) * 64.0;

        int index = int(x) + int(y) * 128 + int(z) * 128 * 128;
        auto it = grid.emplace(index, count);
        if(it.second){
            count++;
        }
    }

    cout << "elements: " << count << endl;
    cout << "load factor: " << grid.load_factor() << endl;

    auto end = high_resolution_clock::now();
    long long duration = duration_cast<milliseconds>(end - start).count();
    float seconds = duration / 1000.0f;
    cout << seconds << "s" << endl;
}

int main(){

    findThenInsert();
    insertThenCheckForSuccess();

}

在这两种情况下,地图的大小都是 82901,所以我假设结果完全相同。

查找并放置:0.937s 就位然后检查:1.268s

【问题讨论】:

  • @TheParamagneticCroissant is C++14:“可选的单引号(')可以插入数字之间作为分隔符,编译器会忽略它们。”
  • @ChrisDrew 哦,这很好,没有意识到这一点。
  • @T.C.为什么需要分配?
  • @n.m emplace 需要Args&amp;&amp;... args 你需要用这些参数构造一些东西,然后才能将它与其他键进行比较,据我所知,emplace 的常见实现将在所需的目标中构造(动态分配在内存)并在不需要时将其删除。

标签: c++ c++11


【解决方案1】:

问题在于emplace 的有效关联容器规范即使在失败情况下也需要分配;这种分配和重新分配的成本在 find-then-insert 策略中占主导地位。

这是因为 emplace 被指定为从其转发参数中嵌入构造 value_type(即pair&lt;Key const, T&gt;);只有在构建了该对后,它才能对密钥进行哈希处理以检查它是否已经存在。 (它不能只取第一个参数,因为它可能是std::piecewise_construct。)它也不能在自动存储中构造pair,然后将它移动到一个节点中,因为emplace没有被指定为需要一个可复制甚至可移动的value_type,因此它必须在每次调用时执行潜在的昂贵节点分配。 (请注意,有序关联容器也有同样的问题,但与分配成本相比,探测的 O(log n) 成本更为显着。)

除非您的插入预计在大多数情况下都能成功,否则最好使用 find-then-emplace 而不是 emplace-then-test。你也可以使用insert,只要你确保你调用的是value_type重载而不是转发到emplace的模板。

这(可能)在 C++17 中得到修复,它(应该)具有try_emplace,具有与 emplace 相似的语义,但在失败情况下提高了性能。 (语义上的区别在于映射类型在失败情况下不是 emplace-constructed;这使得例如将 unique_ptr 存储为映射类型成为可能。)

【讨论】:

  • @ChrisDrew 是的,但你必须小心 - insert 的模板重载转发到 emplace。不过,我认为这在 C++17 中也已修复。
  • 请引用您的来源。 Map 类型的容器具有单独的键和值。只有键被比较/散列。无需构造对来散列/比较密钥。 map::find 设法做到了这一点。 (It can't just take the first argument, because that could be std::piecewise_construct.)这部分不清楚。请解释一下。
  • @n.m. map::find 有一个 key_type 参数; emplace 只是有一组未区分的参数,它们被转发给 value_type 的构造函数。这方面的所有来源都是 C++ 标准,子句 [unord.req],特别是表 无序关联容器要求(除了容器)
  • “构造键两次”标准没有说明当键存在时构造 value_type,但它以某种方式推断出它必须被构造。为什么 value_type 可以,而 key 不行?
  • @ChrisDrew 我这里只有 4.9.3,可惜它没有做任何这些(在源代码中检查)。
【解决方案2】:

我认为问题在于您使用的是emplace 而不是insert。问题是关联容器中的 emplace 函数通常为节点分配内存,即使键已经存在。因此,如果您经常替换重复项,则这些内存分配将被浪费。如果您使用 insert 而不是它只会在插入成功时进行内存分配。

Scott Meyers says 只在“容器不会因为重复值而拒绝添加的值”时更喜欢 emplace 函数而不是 insert 函数

我不能完全准确地重现您的结果,但 my testing 表明 insert (不是 emplace) then test 甚至比 find then emplace 更快:

auto it = grid.insert({index, count});

此决定还可能取决于创建值类型的成本。 find 不需要构造值类型,只需要键。但是emplaceinsert 需要键和值类型,因此在创建值的成本很高的情况下,使用 find 可能会更快,并且仅在需要时才创建值。在这种情况下,您的值只是 int,所以我希望 insertemplace 总是能胜过 find-then-emplace。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-27
    • 2018-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-10
    • 1970-01-01
    相关资源
    最近更新 更多