[Boost.Lockfree] Interest in a ringbuffer class
Hi everyone,
Is there any interest in having a lower level ringbuffer type in
Boost.Lockfree ?
The containers provided today in the library can only store homogeneous
types of fixed size.
At work, we have refined many implementations of ringbuffer classes that
allow one to push/pop
raw buffers. Before investing some time into a pull request, I wanted to
know if there's some
interest in such a type. The interface looks like this:
template
hi,
Is there any interest in having a lower level ringbuffer type in Boost.Lockfree ?
the ringbuffer will be basically using the same algorithm as the spsc_queue under the hood, but basically separate 'algorithm' from 'storage', right? afaict the spsc_queue can already be used in shared memory, though zero-copy is limited to pushing movable types and consuming via a functor (haven't checked the code for a while, though). mmap for buffer wrapping would be really cool to have, though ... -- a more generic interface would be useful for some use cases, where you can directly access the storage may be useful for some use cases, but when i originally wrote the class, it didn't include it, as i was a bit afraid that the interface might be a bit too fragile. but i don't have a very strong opinion on this. i suppose, you already had a look at the code of the spsc_queue already? cheers, tim
Is there any interest in having a lower level ringbuffer type in Boost.Lockfree ?
the ringbuffer will be basically using the same algorithm as the spsc_queue under the hood, but basically separate 'algorithm' from 'storage', right?
afaict the spsc_queue can already be used in shared memory, though zero-copy is limited to pushing movable types and consuming via a functor (haven't checked the code for a while, though). mmap for buffer wrapping would be really cool to have, though ...
I did an unusual variation on a lock free ringbuffer at https://github.com/ned14/boost-lite/blob/master/include/ringbuffer_log.hpp#L.... It's policy driven, STL compliant API, types stored must be trivial. The unusual variation part is that unlike a queue, this is a fixed size history. You cannot remove items, only push items. Once full every new item you push removes the last item in the history. Because the atomic counter always rises, iterators to expired items correctly expire. This makes them safe to hand out to others, though of course dereferencing an iterator is racy. I'd agree with Tim that the spsc_queue is too similar to the proposed ringbuffer to make much sense. Any additions need to be more significantly different like say the ringbuffer log mentioned. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
I'd agree with Tim that the spsc_queue is too similar to the proposed ringbuffer to make much sense. Any additions need to be more significantly different like say the ringbuffer log mentioned.
Somehow I failed to sell correctly my idea, but unless I missed something,
it's not possible to store buffers of different sizes inside a spsc_queue.
(of course it is possible if you can afford using some external storage,
like the heap)
The proposed ringbuffer is quite different in that is only stores raw
buffers,
and allow one to do that without any extra copy.
For example, one could read from a socket in one thread, while handling
them in another:
if (auto buf = rb.start_write(1500)) {
ssize_t read_size = ::read(fd, &buf.data[0], 1500);
if (read_size > 0)
rb.commit_write(read_size);
}
Of course, one could use a `spsc_queue
the ringbuffer will be basically using the same algorithm as the spsc_queue under the hood, but basically separate 'algorithm' from 'storage', right?
That's right, but that's not the main selling point. The idea is to be able to store raw buffers of different sizes, without any external storage or indirection. When you do buffer* buf = rb.start_write(10); The variable `buf' is stored inside the ring buffer, so that the commit operation only change the position of the write cursor. One possible way to do that is to define buffer as struct buffer { size_type size; byte_type data[]; }; a more generic interface would be useful for some use cases, where you
can directly access the storage may be useful for some use cases, but when i originally wrote the class, it didn't include it, as i was a bit afraid that the interface might be a bit too fragile. but i don't have a very strong opinion on this.
The interface is quite low level, and mostly meant to be used by subclass. Maybe I should have started with that, but the range of problems I'm trying to solve boil down to passing network packets or messages from one thread to another. This could also apply to audio software in general, were the producer and the consumer work in different threads at different pace.
i suppose, you already had a look at the code of the spsc_queue already?
Yes ! I was a bit confused as there is a class called basic_ringbuffer, but it's mostly there to share some common code, right ? -- Raphaël Londeix http://hotgloupi.fr
On 26/01/2017 10:56, Raphaël Londeix wrote:
the ringbuffer will be basically using the same algorithm as the spsc_queue under the hood, but basically separate 'algorithm' from 'storage', right?
That's right, but that's not the main selling point. The idea is to be able to store raw buffers of different sizes, without any external storage or indirection. When you do
buffer* buf = rb.start_write(10);
The variable `buf' is stored inside the ring buffer, so that the commit operation only change the position of the write cursor. One possible way to do that is to define buffer as
Personally speaking, if you're not using fixed size (and usually cache line aligned) items in thread shared memory storage, you've probably got the wrong design. For example, if I were accumulating UDP packets using many threads, I'd keep a ring buffer per thread and timestamp their arrival using RDTSC so they could be reassembled into received order later if needed. That avoids multiple threads modifying the same memory which can become hideously slow. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
The variable `buf' is stored inside the ring buffer, so that the commit operation only change the position of the write cursor. One possible way to do that is to define buffer as
Personally speaking, if you're not using fixed size (and usually cache line aligned) items in thread shared memory storage, you've probably got the wrong design.
Nothing prevents us using something like this: struct buffer { size_type size; size_type capacity; size_type data[]; }; Making it possible to align the buffer object. I'm not sure what you are trying to say, my initial intent was to know whether or not some people might be interested in such a container. Alignment, cache friendliness, false sharing, etc. are all implementation detail. Having one thread that produce some content and another consuming it is really a common pattern. The only different thing I'm trying to address here is that for some problems, you don't have fixed size objects.
For example, if I were accumulating UDP packets using many threads, I'd keep a ring buffer per thread and timestamp their arrival using RDTSC so they could be reassembled into received order later if needed. That avoids multiple threads modifying the same memory which can become hideously slow.
I never said that *multiple* threads should modify the same memory. I've only ever talked about the single producer/single consumer case. Also in your specific example, depending on the underlying protocol, you won't necessarily know the size of your packets, or the upper bound might still be not practical to avoid a significant waste of space. PS: your mails are always in the spam, even though I mark them at not spam each time. -- Raphaël Londeix http://hotgloupi.fr
PS: your mails are always in the spam, even though I mark them at not spam each time.
https://groups.google.com/forum/#!topic/boost-steering/EcKn2yA9ip4 -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 26 January 2017 at 15:21, Niall Douglas
PS: your mails are always in the spam, even though I mark them at not spam each time.
https://groups.google.com/forum/#!topic/boost-steering/EcKn2yA9ip4
FYI, I use GMail and curiously only Niall Douglas' messages end up in spam, constantly and despite I've been very stubborn marking them as not a spam. All the other mailing to Boost lists end up in my Inbox. Best regards, -- Mateusz Loskot, http://mateusz.loskot.net
On 01/26/17 19:59, Mateusz Loskot wrote:
On 26 January 2017 at 15:21, Niall Douglas
wrote: PS: your mails are always in the spam, even though I mark them at not spam each time.
https://groups.google.com/forum/#!topic/boost-steering/EcKn2yA9ip4
FYI, I use GMail and curiously only Niall Douglas' messages end up in spam, constantly and despite I've been very stubborn marking them as not a spam. All the other mailing to Boost lists end up in my Inbox.
There is a checkbox named "Never mark as Spam" or something like that in GMail filters. I have it checked, and I don't think I ever had any problems with messages from Boost ML ending up in Spam.
On 26/01/2017 23:56, Raphaël Londeix wrote:
That's right, but that's not the main selling point. The idea is to be able to store raw buffers of different sizes, without any external storage or indirection. When you do
buffer* buf = rb.start_write(10);
The variable `buf' is stored inside the ring buffer, so that the commit operation only change the position of the write cursor. One possible way to do that is to define buffer as
struct buffer { size_type size; byte_type data[]; };
That sounds a bit different from what I was imagining. I have a somewhat similar ringbuffer interface (in terms of separated reserve and commit) but it assumes single-copy inside the methods; it never exposes internal buffer memory (because zero-copy gets tricky if you want to span the ringbuffer's wrap point). I've only ever used it as a block-of-bytes buffer, typically in cases where at least one end doesn't care about message boundaries and can just process bytes in arbitrary chunks.
the ringbuffer will be basically using the same algorithm as the spsc_queue under the hood, but basically separate 'algorithm' from 'storage', right?
That's right, but that's not the main selling point. The idea is to be able to store raw buffers of different sizes, without any external storage or indirection.
don't get me wrong, i definitely see the use case for this! i'm wondering about a good way to unify this under a common API. e.g. i wonder if the spsc_queue could be modified (or specialized for uint8_t), and provide an API that exposes the internal memory layout to the user. i also like the idea to use memory mapping for unwrapping. but again, we need to find a way to formulate this in a nice API and make it cross-platform (i don't have any experience on this)
i suppose, you already had a look at the code of the spsc_queue already?
Yes ! I was a bit confused as there is a class called basic_ringbuffer, but it's mostly there to share some common code, right ?
well, the semantics is a fixed-size single-producer/single consumer wait-free queue, which is implemented via a ringbuffer. it basically encapsulates the algorithm without knowing how the memory is allocated ...
don't get me wrong, i definitely see the use case for this! i'm wondering about a good way to unify this under a common API.
e.g. i wonder if the spsc_queue could be modified (or specialized for uint8_t), and provide an API that exposes the internal memory layout to the user.
Ok, if you agree that it's worth having, then we can discuss about the design :) Let me create a gist or something with more than an interface as a basis for the discussion. i also like the idea to use memory mapping for unwrapping. but again, we
need to find a way to formulate this in a nice API and make it cross-platform (i don't have any experience on this)
I think that this could easily be integrated in boost.interprocess once we have a solid basic type for ringbuffers. I believe that we can cover all cases by making this type policy driven. Cheers, -- Raphaël Londeix http://hotgloupi.fr
Hi guys,
I created a first draft
https://gist.github.com/hotgloupi/0e970a0e11091dfb0df8f942a0352b87
There's basically four types:
struct default_ringbuffer_policies;
template<typename Policies> struct basic_ringbuffer;
template<typename T> struct default_ring_policies;
template
participants (6)
-
Andrey Semashev
-
Gavin Lambert
-
Mateusz Loskot
-
Niall Douglas
-
Raphaël Londeix
-
Tim Blechmann