6

I want to create unordered_map(Because I specifically want a hash map). I want to allocate its max size (according to my constraints) in the beginning.
So, if I want to allocated 256 entries, and the size of each entry is 1B (just an example. Let's say 1Byte includes the Key and the Value). Then the total size of my unordered_map keys + entries is 256B. I want to pre-allocate 256B in the allocator.
Then, when the unordered_map will call allocate()/deallocate(), the allocator will give it 1B from the already-allocated memory.

typedef boost::unordered::unordered_map<int, MyClass, boost::hash<int>, std::equal_to<MyClass>, ??? > > myMap

Does it exists in BOOST? or somewhere else?

---- edit ----

As I see it (Thanks to the answers here) - there are two solutions for my problem:

  1. Implement an allocator, which holds a boost::pool<>. This pool is built in the allocator constructor. When allocate() is being called from unordered_map, it actually calls pool.malloc(), and when deallocate() is called from unordered_map, it actually calls pool.free().

  2. Use an already implemented allocator, such as pool_allocator like this:

typedef pool_allocator<std::pair<MyKey, MyClass>, boost::default_user_allocator_new_delete, boost::mutex, 1024 >) MyAllocator;
typedef unordered_map<MyKey, MyClass, hash, eq, MyAllocator> MyUnorderedMap;

The seconds option is still unclear to me, because:
a. Can I declare only one MyUnorderedMap?
b. How can I declare a new MyUnorderedMap with different next_block size than 1024 in run time?

hudac
  • 2,584
  • 6
  • 34
  • 57
  • Why cant you supply your own allocator (i.e. from any library) in the last `Alloc` template param? Is some constraint preventing you from doing this? – Preet Kukreti Mar 08 '15 at 12:57
  • I can write my own allocator. I thought it already exists in BOOST because it looks like a general request. – hudac Mar 08 '15 at 14:23

1 Answers1

4

What you describe can actually only achieved with something like Boost Intrusive "Maps" (actually, sets then).

However to get truly 1B - allocated elements you'd need to define a custom stateful value traits, so you can store the node-index metadata separately from the element payload.

However, from the fact that you claim the element type to be 1B (which can obviously never be true for a concrete key and value type), I'll not assume you actually wanted this contrived solution for "some reason".

Instead, let me suggest three more mundane approaches:

  • Using a flat_map
  • Using a Boost Intrusive unordered set
  • Using an unordered set with Boost Pool fixed size allocator¹

Boost flat_map

If hash lookup is not mandatory, you can simplify a lot by just reserving contiguous element storage up front and storing an ordered map instead:

Live On Coliru

#include <boost/container/flat_map.hpp>
#include <iostream>

using Elements = boost::container::flat_map<std::string, std::string>;

int main() {
    Elements map;
    map.reserve(256); // pre-allocate 256 "nodes"!

    map.insert({
            { "one",   "Eins"  },
            { "two",   "Zwei"  },
            { "three", "Drei"  },
            { "four",  "Vier"  },
            { "five",  "Fuenf" },
        });

    for (auto& e : map) {
        std::cout << "Entry: " << e.first << " -> " << e.second << "\n";
    }

    std::cout << "map[\"three\"] -> " << map["three"] << "\n";
}

Prints

Entry: five -> Fuenf
Entry: four -> Vier
Entry: one -> Eins
Entry: three -> Drei
Entry: two -> Zwei
map["three"] -> Drei

Boost Intrusive

CAVEAT Intrusive containers come with their own set of trade offs. Managing the underlying storage of the elements can be error-prone. Auto-link behaviour of the hooks inhibits the constant-time implementation of size() and similar (empty() on some of the unordered set configurations) so this might not be your thing.

Live On Coliru

#include <boost/intrusive/unordered_set.hpp>
#include <boost/intrusive/unordered_set_hook.hpp>
#include <iostream>

namespace bi = boost::intrusive;

struct Element;

namespace boost {
    template <> struct hash<Element> {
        size_t operator()(Element const& e) const;
    };
}

struct Element : bi::unordered_set_base_hook<> {
    std::string key;
    mutable std::string value;

    Element(std::string k = "", std::string v = "") 
        : key(std::move(k)), value(std::move(v)) { }

    bool operator==(Element const& other) const { return key == other.key; }
};

size_t boost::hash<Element>::operator()(Element const& e) const {
    return hash_value(e.key); 
}

using Elements = bi::unordered_set<Element>;

int main() {
    std::array<Element, 256> storage;               // reserved 256 entries
    std::array<Elements::bucket_type, 100> buckets; // buckets for the hashtable

    Elements hashtable(Elements::bucket_traits(buckets.data(), buckets.size()));

    storage[0] = { "one",   "Eins"  };
    storage[1] = { "two",   "Zwei"  };
    storage[2] = { "three", "Drei"  };
    storage[3] = { "four",  "Vier"  };
    storage[4] = { "five",  "Fuenf" };

    hashtable.insert(storage.data(), storage.data() + 5);

    for (auto& e : hashtable) {
        std::cout << "Hash entry: " << e.key << " -> " << e.value << "\n";
    }

    std::cout << "hashtable[\"three\"] -> " << hashtable.find({"three"})->value << "\n";
}

Prints

Hash entry: two -> Zwei
Hash entry: four -> Vier
Hash entry: five -> Fuenf
Hash entry: three -> Drei
Hash entry: one -> Eins
hashtable["three"] -> Drei

Pool fixed size allocator¹

If you absolutely require the node-based storage, consider using a custom allocator.

¹ You'll note that (at least with Boost's unordered_map implementation) the allocator is used for two types (bucket pointers and value nodes) and as such there are two fixed size allocations possible.

(See the cleanup calls at the bottom of the sample)

Live On Coliru

#include <boost/pool/pool_alloc.hpp>
#include <boost/unordered/unordered_map.hpp>
#include <iostream>

using RawMap = boost::unordered_map<std::string, std::string>;
using Elements = boost::unordered_map<
        std::string, std::string,
        RawMap::hasher, RawMap::key_equal,
        boost::fast_pool_allocator<RawMap::value_type>
    >;

int main() {
    {
        Elements hashtable;

        hashtable.insert({
                { "one",   "Eins"  },
                { "two",   "Zwei"  },
                { "three", "Drei"  },
                { "four",  "Vier"  },
                { "five",  "Fuenf" },
                });

        for (auto& e : hashtable) {
            std::cout << "Hash entry: " << e.first << " -> " << e.second << "\n";
        }

        std::cout << "hashtable[\"three\"] -> " << hashtable.find("three")->second << "\n";
    }

    // OPTIONALLY: free up system allocations in fixed size pools
    // Two sizes, are implementation specific. My 64 system has the following:
    boost::singleton_pool<boost::fast_pool_allocator_tag, 8>::release_memory();  // the bucket pointer allocation
    boost::singleton_pool<boost::fast_pool_allocator_tag, 32>::release_memory(); // the ptr_node<std::pair<std::string const, std::string> >
}
Community
  • 1
  • 1
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Why not using boost::pool<> ? – hudac Mar 09 '15 at 12:14
  • A pool is a pool, and an allocator is an allocator. I might be missing a thing here, but you can't make STL containers use a pool, AFAIK – sehe Mar 09 '15 at 13:30
  • I can write an allocator which uses the pool, and give that to the stl... If there was already this allocator It would be good.. (I couldn't understand it from your answer :\ ) – hudac Mar 09 '15 at 13:36
  • 1
    This is already this allocator. I don't get what is not clear. Code talks. Perhaps you're confused about `pool` vs `singleton_pool` (which underlies the `pool_allocator`). If so, yes it seems the corresponding stateful allocator (for non-singleton pool) is missing. This is likely because stateful allocators are a C++11 feature. (Perhaps you can can cobble it together with Boost Container library if you need C++03 support though). Background: http://www.boost.org/doc/libs/1_57_0/doc/html/container/Cpp11_conformance.html#container.Cpp11_conformance.alloc_traits_move_traits – sehe Mar 09 '15 at 13:39
  • I need to understand couple of things: 1. `flat_map::reserve()`, does it allocates the bucket pointers only? or the value nodes also? 2. Looks like I want to create the pool itself, so I don't want to allocate the memory in the stack - or something similar to the intrusive example (I want to allocate it in a pool somewhere). 3. `singleton_pool`, How can I pre-allocate the memory? `(pool_allocator, boost::default_user_allocator_new_delete, boost::mutex, 1024 >)`. Can I have only one `unordered_map` because it uses the `singleton_pool`? – hudac Mar 09 '15 at 16:02
  • @hudac flat_map is an ordered container with contiguous element storage (so like a sorted `std::vector>`). The reason I mention it is because you get the "upfront 1-time allocation" you desire for free. Of course the cost is reduced performance on insertion/deletion. But lookup times are frequently (much) higher than with `std::map` – sehe Mar 09 '15 at 16:03
  • So, that's why I gave the other approaches. Really, as I explained, your question didn't make a lot of sense. I went out of my way to give you inspiration on what you _can_ do, with various trade-offs. No need to tell me that not all of them fit. In fact, I don't particularly care if none of them fit. You may improve the question. – sehe Mar 09 '15 at 16:09
  • I wasn't meant to say that your answer didn't fit, It just confused me - so I wanted to understand better by writing these comments. Eventually it seems that what I aimed for is the 3'rd part of the answer. But I can't fully understand it. – hudac Mar 09 '15 at 17:13
  • @hudac late delivery: I happen to have "cobbled it together" using boost container indeed: http://stackoverflow.com/a/36830643/85371 about a month ago – sehe May 11 '16 at 16:18