std::map try emplace vs emplace strange behaviour

huangapple go评论70阅读模式
英文:

std::map try emplace vs emplace strange behaviour

问题

通常的建议是在几乎所有情况下,优先使用 std::map::try_emplace 而不是 std::map::emplace

我编写了一个简单的测试来追踪在调用这些函数时对象的创建、复制、移动和销毁,有冲突和没有冲突的情况下,结果显示当键不在映射中时,try_emplace 会导致键多一次移动和销毁。

为什么会有这种行为差异?

我知道移动和销毁已移动的对象通常是廉价的,尤其对于简单的键来说更是如此,但是我仍然对结果感到惊讶,因为它们似乎暗示在某些情况下,emplace 可能更有效率。

Compiler explorer 链接 (Clang 14, libc++, -O3)

源代码:

#include <map>
#include <iostream>

struct F {
    F(int i): i(i) { std::cout << "- ctor (" << i << ")\n"; }
    ~F() { std::cout << "- dtor (" << i << ")\n"; }
    F(const F& f): i(f.i) { std::cout << "- copy ctor (" << i << ")\n"; }
    F(F&& f): i(f.i) { std::cout << "- move ctor (" << i << ")\n"; }
    F& operator=(const F& f) { i = f.i; std::cout << "- copy (" << i << ")\n"; return *this; }
    F& operator=(F&& f) { i = f.i; std::cout << "- move (" << i << ")\n"; return *this; }
    bool operator <(const F& f) const { return i < f.i; }
    int i{};
};

int main() {
    std::map<F, F> m;
    std::cout << "emplace 1:\n";
    m.emplace(1, 2);
    std::cout << "emplace 2:\n";
    m.emplace(1, 3);
    std::cout << "clear:\n";
    m.clear();
    std::cout << "try_emplace 1:\n";
    m.try_emplace(1, 2);
    std::cout << "try_emplace 2:\n";
    m.try_emplace(1, 3);
    std::cout << "done:\n";
}

结果:

emplace 1:
- ctor (1)
- ctor (2)
emplace 2:
- ctor (1)
- ctor (3)
- dtor (3)
- dtor (1)
clear:
- dtor (2)
- dtor (1)
try_emplace 1:
- ctor (1)
- move ctor (1)
- ctor (2)
- dtor (1)
try_emplace 2:
- ctor (1)
- dtor (1)
done:
- dtor (2)
- dtor (1)
英文:

The common advice is to prefer using std::map::try_emplace in preference to std::map::emplace in almost every instance.

I wrote a simple test to trace object creation/copying/moving/destruction when calling those functions, with and without clashes, and the results show that try_emplace incurs an extra move and destruction of the key when it is not already in the map.

Why the difference in behaviour?

I do know that moves and destructions of moved-from objects are usually cheap, especially so for trivial keys, but I was still surprised by the results as they seem to imply that for some cases emplace might be more efficient.

Compiler explorer link (Clang 14, libc++, -O3)

Source:

#include &lt;map&gt;
#include &lt;iostream&gt;

struct F {
    F(int i): i(i) { std::cout &lt;&lt; &quot;- ctor (&quot; &lt;&lt; i &lt;&lt; &quot;)\n&quot;; }
    ~F() { std::cout &lt;&lt; &quot;- dtor (&quot; &lt;&lt; i &lt;&lt; &quot;)\n&quot;; }
    F(const F&amp; f): i(f.i) { std::cout &lt;&lt; &quot;- copy ctor (&quot; &lt;&lt; i &lt;&lt; &quot;)\n&quot;; }
    F(F&amp;&amp; f): i(f.i) { std::cout &lt;&lt; &quot;- move ctor (&quot; &lt;&lt; i &lt;&lt; &quot;)\n&quot;; }
    F&amp; operator=(const F&amp; f) { i = f.i; std::cout &lt;&lt; &quot;- copy (&quot; &lt;&lt; i &lt;&lt; &quot;)\n&quot;; return *this; }
    F&amp; operator=(F&amp;&amp; f) { i = f.i; std::cout &lt;&lt; &quot;- move (&quot; &lt;&lt; i &lt;&lt; &quot;)\n&quot;; return *this; }
    bool operator &lt;(const F&amp; f) const { return i &lt; f.i; }
    int i{};
};

int main() {
    std::map&lt;F, F&gt; m;
    std::cout &lt;&lt; &quot;emplace 1:\n&quot;;
    m.emplace(1, 2);
    std::cout &lt;&lt; &quot;emplace 2:\n&quot;;
    m.emplace(1, 3);
    std::cout &lt;&lt; &quot;clear:\n&quot;;
    m.clear();
    std::cout &lt;&lt; &quot;try_emplace 1:\n&quot;;
    m.try_emplace(1, 2);
    std::cout &lt;&lt; &quot;try_emplace 2:\n&quot;;
    m.try_emplace(1, 3);
    std::cout &lt;&lt; &quot;done:\n&quot;;
}

Results:

emplace 1:
- ctor (1)
- ctor (2)
emplace 2:
- ctor (1)
- ctor (3)
- dtor (3)
- dtor (1)
clear:
- dtor (2)
- dtor (1)
try_emplace 1:
- ctor (1)
- move ctor (1)
- ctor (2)
- dtor (1)
try_emplace 2:
- ctor (1)
- dtor (1)
done:
- dtor (2)
- dtor (1)

答案1

得分: 5

以下是翻译好的部分:

两个函数之间的区别是:

  • std::map::emplace 在原地构造一个 value_type,即一个 std::pair,然后尝试插入这个对。
  • std::map::try_emplace 首先尝试找到插入位置,如果找到了,它将在原地构造 value_type,即 std::pair

try_emplace 看起来可以为我们节省一些工作,但请记住,即使我们使用 try_emplace 不向地图插入任何新内容,我们仍然需要检查是否可以插入以及在哪里插入。这默认使用 std::less 来完成,最终调用以下函数:

bool operator<(const F& f) const { return i < f.i; }

为了执行此比较,必须存在一个 F 对象,并且在 try_emplace 中会构造一次。

让我们使用正在发生的情况来注释您的示例:

emplace 1:
 - 构造函数 (1) // 构造 node{1, 2}
 - 构造函数 (2)
emplace 2:
 - 构造函数 (1) // 构造 node{1, 3}
 - 构造函数 (3)
 - 析构函数 (3) // 无法插入,销毁 node{1, 3}
 - 析构函数 (1)

clear:
 - 析构函数 (2) // 销毁 node{1, 2}
 - 析构函数 (1)

try_emplace 1:
 - 构造函数 (1) // 构造临时键
 - 移动构造函数 (1) // 找到位置,将键移动到 node
 - 构造函数 (2) // 构造值
 - 析构函数 (1) // 销毁临时键
try_emplace 2:
 - 构造函数 (1) // 构造临时键
 - 析构函数 (1) // 销毁临时键
done:
 - 析构函数 (2) // 销毁 node{1, 2}
 - 析构函数 (1)

结论

我们不能说 emplacetry_emplace 通常更好或更差,而是存在权衡:

无预先存在的键 插入成功 插入失败
emplace 零额外开销<sup>1)</sup> 浪费键和值的初始化
try_emplace 浪费键的移动 浪费键的初始化

当我们有一个预先存在的键而不是在 try_emplaceemplace 中初始化键时,权衡会发生变化:

预先存在的键 插入成功 插入失败
emplace 浪费键的移动 浪费键的移动和初始化,浪费值的初始化
try_emplace 浪费键的移动 零额外开销

总之,与 emplace 相比,try_emplace 最坏的情况下浪费一个移动构造函数,最好的情况下严格更好。在大多数情况下更喜欢它,但不是所有情况都是如此。

英文:

The difference between the two functions is:

  • std::map::emplace constructs a value_type, i.e. a std::pair in-place, and then attempts to insert this pair.
  • std::map::try_emplace attempts to find an insert location first, and if one is found, it will construct the value_type, i.e. a std::pair in-place.

try_emplace appears to save us some work, but remember that even if we don't insert anything new into the map with try_emplace, we still have to check whether we can, and where. This happens with std::less by default, which ends up calling:

bool operator&lt;(const F&amp; f) const { return i &lt; f.i; }

An F object must exist to perform this comparison, and is being constructed once during try_emplace.

Let's annotate your example with what is happening:

emplace 1:
 - ctor (1) // construct node{1, 2}
 - ctor (2)
emplace 2:
 - ctor (1) // construct node{1, 3}
 - ctor (3)
 - dtor (3) // can&#39;t insert, destroy node{1, 3}
 - dtor (1)

clear:
 - dtor (2) // destroy node{1, 2}
 - dtor (1)

try_emplace 1:
 - ctor (1) // construct temporary key
 - move_ctor (1) // location found, move key to node
 - ctor (2) // construct value
 - dtor (1) // destroy temporary key
try_emplace 2:
 - ctor (1) // construct temporary key
 - dtor (1) // destroy temporary key
done:
 - dtor (2) // destroy node{1, 2}
 - dtor (1)

Conclusion

We can't say that emplace is universally better or worse than try_emplace, rather there is a trade-off:

No Pre-Existing Key Insertion Success Insertion Failure
emplace zero overhead<sup>1)</sup> wasted key and value initialization
try_emplace wasted key move wasted key initialization

The trade-off changes when we have a pre-existing key instead of initializing one inside of try_emplace or emplace:

Pre-Existing Key Insertion Success Insertion Failure
emplace wasted key move wasted key move & init, wasted value init
try_emplace wasted key move zero overhead

In conclusion, try_emplace is at worst wasting one move constructor compared to emplace, and at best, it's strictly better. Prefer it in most, but not all cases.


<sup>1)</sup> "overhead" is relative to magically knowing the right location (or absence thereof) in the map and initializing a node in-place. Initialization of the pre-existing key is considered free.

huangapple
  • 本文由 发表于 2023年6月15日 05:08:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76477550.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定