performance of emplace is worse than check followed by emplace

独自空忆成欢 提交于 2019-12-03 12:08:29

Your code is unfortunately optimal for the standard library as it currently is.

The problem is that the emplace operation is designed to avoid copying, not to avoid unnecessary construction of the mapped type. In practical terms, what happens is that the implementation allocates and constructs a node, containing the map value_type i.e. pair<const Key, T>, and then hashes the key to determine whether the constructed node can be linked into the container; if this collides then the node is deleted.

As long as hash and equal_to are not too expensive, your code shouldn't do too much extra work.

An alternative is to use a custom allocator that intercepts 0-argument construction of your mapped type; the problem is that detecting such construction is pretty fiddly:

#include <unordered_map>
#include <iostream>

using Key = int;
struct D {
    D() = delete;
    D(D const&) = delete;
    D(D&&) = delete;
    D(std::string x, int y) { std::cout << "D(" << x << ", " << y << ")\n"; }
};
template<typename T>
struct A {
    using value_type = T;
    using pointer = T*;
    using const_pointer = T const*;
    using reference = T&;
    using const_reference = T const&;
    template<typename U> struct rebind { using other = A<U>; };
    value_type* allocate(std::size_t n) { return std::allocator<T>().allocate(n); }
    void deallocate(T* c, std::size_t n) { std::allocator<T>().deallocate(c, n); }
    template<class C, class...Args> void construct(C* c, Args&&... args) { std::allocator<T>().construct(c, std::forward<Args>(args)...); }
    template<class C> void destroy(C* c) { std::allocator<T>().destroy(c); }

    std::string x; int y;
    A(std::string x, int y): x(std::move(x)), y(y) {}
    template<typename U> A(A<U> const& other): x(other.x), y(other.y) {}
    template<class C, class...A> void construct(C* c, std::piecewise_construct_t pc, std::tuple<A...> a, std::tuple<>) {
        ::new((void*)c)C(pc, a, std::tie(x, y)); }
};

int main() {
    using UM = std::unordered_map<Key, D, std::hash<Key>, std::equal_to<Key>, A<std::pair<const Key, D>>>;
    UM um(0, UM::hasher(), UM::key_equal(), UM::allocator_type("hello", 42));
    um[5];
}

You could use boost::optional<T> in order to be able to default construct the mapped type and then assign an initialized T to it later.

#include <cassert>
#include <unordered_map>
#include <boost/optional.hpp>

struct MappedType
{
  explicit MappedType(int) {}
};

int main()
{
  std::unordered_map<int, boost::optional<MappedType>> map;
  boost::optional<MappedType>& opt = map[0];
  assert(!opt.is_initialized());
  opt = MappedType(2);
  assert(opt.is_initialized());
  MappedType& v = opt.get();
}
Sophit

James, you've mostly answered your own question.

You're doing nothing wrong in either implementation. emplace simply does more work than find, especially when the key already exists in your unordered_map.

If your get_value helper function mostly receives duplicates, then calling emplace every time will cause a performance hot spot as you've observed.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!