Closed ToppDev closed 2 years ago
IMHO adding padding to ScrollingBuffer does not solve the underlying problem: you have a data race in your code. Adding padding without using mutexes is still undefined behaviour and might still show artifacts. I know mutexes are expensive, but it's the only solution that guarantees that the program won't randomly crash if, for example, one thread is reading the begin()
of the buffer while the other thread is updating that exact same pointer.
As there is no iterator in this case, which gets invalidated. The program can't crash I think. The memory stays the same because the buffer is scrolling. Therefore it is guaranteed, that memory is valid. The only thing which happens (because of the data race) is, that the buffer gets data added while trying to draw the old data (which then leads to the lines appearing).
The padding I think in this case also ensures, that I never write the same element in my buffer as I try to read/plot.
I agree, that my padded buffer solution is not ideal, but I try to avoid the expensive mutex here. Data is inserted at very high frequencies in my case and the mutex in the draw function would directly affect the framerate too.
As there is no iterator in this case, which gets invalidated the program can't crash I think
An iterator is just an abstraction. Even if there is no iterator the container has two pointers, one that points to the begin and one that points to the end of the sequence. A pointer is (usually) 8 bytes. Now imagine one thread is reading these 8 bytes while the other thread is writing them. The first thread will read a (probably) invalid pointer.
The memory stays the same because the buffer is scrolling. Therefore it is guaranteed, that memory is valid
Yeah, the memory is the same, but the pointers do change. On x86 writing an 8-byte integer is usually atomic, but this is never guaranteed nor it is portable behaviour. Thus a crash is possible and in fact very likely on non-x86 machines because you have a data race in the code.
The padding I think in this case also ensures, that I never write the same element in my buffer as I try to read/plot.
The padding does solve the data race between the items, that's true. However it doesn't solve the other data race issue, which is that you have two threads simultaneously reading and writing two pointers at the same time. The data race is not only in the insertion of the data itself, but in the updating of the pointers of the buffer too.
I agree, that my padded buffer solution is not ideal, but I try to avoid the expensive mutex here. Data is inserted at very high frequencies in my case and the mutex in the draw function would directly affect the framerate too.
I get it, and I agree that mutexes are somewhat expensive. But perhaps you could use a spinlock instead, or use std::atomic
, or maybe batch the data and update it all at once using a mutex... I 100% agree that mutexes are slow, but for example spdlog uses mutexes and it is able to log over 1.600.000 strings per second. It should be fast enough for 99.9999% of the applications out there.
The padding idea is really good and it solves the data race on insertion/deletion of the items in the ring buffer, but you need something more in order to guarantee that this won't randomly crash because of the begin/end pointers.
I think @marcizhu is giving sound advice in this situation. I would definitely try a mutex before jumping to conclusions. My experience has been that mutexes are sufficient in the vast majority of applications. Another option to consider is a lock-free SPSC queue [1], [2]. There would be some copying involved (i.e. you need to pop elements off the queue and copy them into a local buffer in the ImPlot thread), but you can at leaest avoid having the producer thread wait on the ImPlot plot function to unlock a mutex.
I will carefully review your proposal on the offset/stride, but my initial reaction is that this would be an API altering change that may cause more problems than it potentially fixes.
@ToppDev , I suggest you read up on lock-free queues and false sharing if you aren't familiar with the concept. Your proposal is logical, but this is a far more nuanced propblem than you might initially expect.
thank you both for your detailed explanation and suggestions on how to solve this.
I agree, that I did not understand the depth of the problem with the start and end pointer and that I subconsciously relied on the compiler making certain things possible, while he is not forced to do so. I will definitely read up on lock-free queues and false sharing, as it is actually the first time hearing the term for me :smile:
Thank you both again for your time. I see that this change would introduce a breaking change to the API and therefore its better for me to solve it 'the right way' than trying to win the race with cheats.
From my side this issue can be closed then. Feel free to close it or I will close it in a few days just in case someone wants to answer as well.
Feel free to reopen this or come back with questions if you need!
I agree with @epezent, feel free to come back with questions if you have them, we're always glad to help đŸ˜„
Hello,
this is a feature request, as I don't think the library can handle this yet (tell me if otherwise).
Problem description
When inserting data in a ScrollingBuffer (implementation from the Demo), the following behavior occurs
The plot often connects the start to the end of the circle and therefore causing weird lines to appear. I insert the data in a different thread than the plot thread, and that is why this happens.
Solution
I know that implot does not support multithreading, so one solution would be, to use mutexes, in order to make the data insertion and plotting thread safe. This however causes a lot of overhead, which I think can be avoided.
The solution I came up with is, to insert padding elements in my ScrollingBuffer. A quick example will explain what I mean with padding. (
s
means the start of the data,e
is one after the last and marks the next insert position)Normal (unpadded) ScrollingBuffer
Padded ScrollingBuffer
The padded buffer has the advantage, that the old value is not directly overridden and therefore the circle would not connect the ends of the circle, but rather just plot a value which is one insertion old.
Improvements for ImPlot needed to make this work
The current behavior of the offset and the buffer is that it starts from the
offset
readingsize - offset
elements and the jumping back to the start pointer and then reading tilloffset
Needed would be an ability to tell the Getter how many elements come after the offset and not automatically calculate it with the formula
size - offset
. I don't have in-depth knowledge of the ImPlot library so I don't know how this could be done the easiest way.Additional info / resources
Padded ScrollingBuffer implementation: https://compiler-explorer.com/z/6WPP3WTG6
Code in case compiler-explorer goes down
```C++ #include