1

I am using a boost::interprocess::deque with a memory_mapped_file as a file buffer that lets data survive reboots etc.

I am creating the buffer like this:

typedef boost::interprocess::allocator<char, boost::interprocess::managed_mapped_file::segment_manager> char_allocator_type;
typedef boost::interprocess::basic_string<char, std::char_traits<char>, char_allocator_type> persisted_string_type;
typedef boost::interprocess::allocator<persisted_string_type, boost::interprocess::managed_mapped_file::segment_manager> persisted_string_allocator_type;
typedef boost::interprocess::deque<persisted_string_type , persisted_string_allocator_type> deque_buf;

auto* mmf = new boost::interprocess::managed_mapped_file(boost::interprocess::open_or_create, "file_name", 1000000);
auto* buffer = mmf.find_or_construct<deque_buf>(boost::interprocess::unique_instance)(mmf_->get_segment_manager());

I am writing to the back of the buffer like this:

try {
   char_allocator_type ca(mmf->get_segment_manager());
   persisted_string_type persisted_string(ca);
   persisted_string = "some string";
   buffer->push_back(persisted_string);
} catch (const boost::interprocess::bad_alloc &e) {
   //buffer full, handle it
}

I am erasing from the front of the buffer like this:

buffer->pop_front();

I use it like a queue. So whenever I cannot add new data to the back I erase data from the front until I can add the new data. But, as it runs I have to erase more and more data from the front in order to add the new data in the back. Ultimately the deque can hardly contain any elements. Increasing mapped file size only postpone the problem.

What am I doing wrong?

Rgds Klaus

1 Answers1

1

Firstly: add some peace and quiet to your code :)

#include <boost/container/scoped_allocator.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/deque.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>

namespace bip = boost::interprocess;
namespace bc = boost::container;

using Segment = bip::managed_mapped_file;
using Mgr     = Segment::segment_manager;
template <typename T>
using Alloc = bc::scoped_allocator_adaptor<bip::allocator<T, Mgr>>;

using String =
    bip::basic_string<char, std::char_traits<char>, Alloc<char>>;

template <typename T> using Deque = bip::deque<T, Alloc<T>>;

This makes for expressive code. The scoped_allocator_adaptor reduces the number of times you're explicitly (inefficiently) shuffling around allocator instances:

int main() {
    using Buffers = Deque<String>;

    bip::managed_mapped_file mmf(bip::open_or_create, "file_name", 1 << 20); // 1 MiB

    auto& buffer = *mmf.find_or_construct<Buffers>(bip::unique_instance)(
        mmf.get_segment_manager());

    try {
       buffer.emplace_back("some string");
    } catch (const boost::interprocess::bad_alloc &e) {
       //buffer full, handle it
    }
}

The Problem

The problem is likely fragmentation. There's no real solution here unless you can afford to periodically repopulate the shared memory segment from fresh.

Another part is the segment manager overhead:

One thing to consider is using a pool for the strings. By using a fixed-size allocator you will be largely avoiding fragmentation issues.

Also keep in mind the use of reserve() and shrink_to_fit() to guide the implementation on how to manage actual allocation patterns.

Circular Buffers

But a more direct match to your use-case would seem to be a ring buffer. I'd sketch something like:

using Buffer = boost::container::small_vector<char, 100, Alloc<char>>;

template <typename T> using Ring = boost::circular_buffer<T, Alloc<T>>;

If you know the maximum size of your buffers before hand you could use a static_vector instead:

using Buffer = boost::container::static_vector<char, 100>;

Static vector never allocates, so it doesn't even require the allocator.

circular_buffer doesn't quite work as seemlessly with the scoped allocators, so it's a little more painful to use, but probably still more elegant than the manual allocator shuffling you had.

Live On Coliru

#include <boost/circular_buffer.hpp>
#include <boost/container/small_vector.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>

namespace bip = boost::interprocess;
namespace bc = boost::container;

using Segment = bip::managed_mapped_file;
using Mgr     = Segment::segment_manager;
template <typename T>
using Alloc       = bip::allocator<T, Mgr>;
using Buffer      = boost::container::small_vector<char, 100, Alloc<char>>;
using BufferAlloc = Buffer::allocator_type;

template <typename T> using Ring = boost::circular_buffer<T, Alloc<T>>;

static inline std::string_view as_sv(Buffer const& b) {
    return {b.data(), b.size()};
}

#include <iomanip>
#include <iostream>
#include <ranges>
namespace v = std::ranges::views;
using std::ranges::subrange;

int main()
{
    using Buffers = Ring<Buffer>;

    bip::managed_mapped_file mmf(bip::open_or_create, "file_name", 1 << 20); // 1 MiB

    auto& sequence = *mmf.find_or_construct<size_t>(bip::unique_instance)(0);
    auto& buffers = *mmf.find_or_construct<Buffers>(bip::unique_instance)(
        1000, // 1000 buffers capacity
        mmf.get_segment_manager());

    auto to_buffer =
        [a = BufferAlloc(buffers.get_allocator())](std::string_view str) {
            return Buffer(str.begin(), str.end(), a);
        };

    int64_t num = buffers.size(); // signed!
    std::cout << "Current size: " << buffers.size() << " ...";

    auto last5 = subrange(buffers) | v::drop(std::max(0l, num - 5));
    for (auto b : last5 | v::transform(as_sv)) {
        std::cout << " " << std::quoted(b);
    }
    std::cout << std::endl;

    try {
       buffers.push_back(to_buffer("some string #" + std::to_string(++sequence)));
    } catch (const boost::interprocess::bad_alloc& e) {
        // buffer full, handle it
        std::cerr << e.what() << "\n";
        return 1;
    }
}

Note how this already has the pop_front behaviour without you needing to anything:

rm file_name; for a in {1..10000}; do if ./sotest; then true; else break; fi; done | nl

Prints

     1  Current size: 0 ...
     2  Current size: 1 ... "some string #1"
     3  Current size: 2 ... "some string #1" "some string #2"
     4  Current size: 3 ... "some string #1" "some string #2" "some string #3"
     5  Current size: 4 ... "some string #1" "some string #2" "some string #3" "some string #4"
     6  Current size: 5 ... "some string #1" "some string #2" "some string #3" "some string #4" "some string #5"
     7  Current size: 6 ... "some string #2" "some string #3" "some string #4" "some string #5" "some string #6"
     8  Current size: 7 ... "some string #3" "some string #4" "some string #5" "some string #6" "some string #7"
     9  Current size: 8 ... "some string #4" "some string #5" "some string #6" "some string #7" "some string #8"
    10  Current size: 9 ... "some string #5" "some string #6" "some string #7" "some string #8" "some string #9"
    11  Current size: 10 ... "some string #6" "some string #7" "some string #8" "some string #9" "some string #10"
    12  Current size: 11 ... "some string #7" "some string #8" "some string #9" "some string #10" "some string #11"
    13  Current size: 12 ... "some string #8" "some string #9" "some string #10" "some string #11" "some string #12"
    14  Current size: 13 ... "some string #9" "some string #10" "some string #11" "some string #12" "some string #13"
...
   998  Current size: 997 ... "some string #993" "some string #994" "some string #995" "some string #996" "some string #997"
   999  Current size: 998 ... "some string #994" "some string #995" "some string #996" "some string #997" "some string #998"
  1000  Current size: 999 ... "some string #995" "some string #996" "some string #997" "some string #998" "some string #999"
  1001  Current size: 1000 ... "some string #996" "some string #997" "some string #998" "some string #999" "some string #1000"
  1002  Current size: 1000 ... "some string #997" "some string #998" "some string #999" "some string #1000" "some string #1001"
  1003  Current size: 1000 ... "some string #998" "some string #999" "some string #1000" "some string #1001" "some string #1002"
  1004  Current size: 1000 ... "some string #999" "some string #1000" "some string #1001" "some string #1002" "some string #1003"
  1005  Current size: 1000 ... "some string #1000" "some string #1001" "some string #1002" "some string #1003" "some string #1004"
  1006  Current size: 1000 ... "some string #1001" "some string #1002" "some string #1003" "some string #1004" "some string #1005"
  1007  Current size: 1000 ... "some string #1002" "some string #1003" "some string #1004" "some string #1005" "some string #1006"
...
  9998  Current size: 1000 ... "some string #9993" "some string #9994" "some string #9995" "some string #9996" "some string #9997"
  9999  Current size: 1000 ... "some string #9994" "some string #9995" "some string #9996" "some string #9997" "some string #9998"
 10000  Current size: 1000 ... "some string #9995" "some string #9996" "some string #9997" "some string #9998" "some string #9999"
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Managed to get the demo **[Live On Coliru](http://coliru.stacked-crooked.com/a/539f92378315b416)** – sehe Nov 10 '21 at 17:15
  • And a strictly simpler and more efficient version [using `static_vector`](http://coliru.stacked-crooked.com/a/04cba9e239463ef0) as well – sehe Nov 10 '21 at 17:18
  • Thank you very much for your input. ```buffer.emplace_back("some string");``` did it for me. I cannot say whether it is due to fragmentation or due to the string being allocated multiple times in the mapped file. But it works! – Klaus Holst Jacobsen Nov 11 '21 at 10:56
  • I have thought about using the boost circular_buffer....as this a the actual functionality that I want. There is however the drawback that you create the circular buffer with a given predefined capacity. I want the managed file size to dictate the capacity. As the strings I add are of unknown and variable size the capacity will vary over time. This behaviour is, AFAIK, not given out of the box with circular_buffer – Klaus Holst Jacobsen Nov 11 '21 at 11:02
  • How does that help? The managed segment is also created with a fixed capacity. As you can see in my example, it's perfectly fine to use varying size elements with circular_buffer (you can easily just use `string` as well, but I wanted to show-case the more allocation-optimized containers at the same time). Of course, then you **might** get your original problem of fragmentation again (if the sizes vary wildly). Again, there's no _real_ solution except perhaps using a pool allocator - or use something other than string that doesn't insist on everything being contiguous in memory. – sehe Nov 11 '21 at 12:17
  • Re: "_`buffer.emplace_back("some string");` did it for me_" - huh surprise. Yeah, you must have been allocating copies that were immediately freed, but leaving "holes" in the segment heap. – sehe Nov 11 '21 at 12:19
  • There are some "under the hood" challenges with fragmentation that I do not understand. But one thing that puzzles me is, if I have removed all entries in my memory segment, why can't I then add a new one. I mean, all the memory is freed, available and contiguous? – Klaus Holst Jacobsen Nov 11 '21 at 19:36
  • If you can show me a minimal reproducer of that, I can help figure out the reason, or perhaps whether there's a bug/unexpected feature involved. – sehe Nov 11 '21 at 21:41