6 ms·
Show HN: Embedded Ring Buffer C++98
- Bambo 5y agoLooking for CC, advice and bugs!
- keithwinstein 5y agoLooks reasonable! On a system with virtual memory, one popular trick is the "virtual ring buffer," which lets the reader and writer always access a contiguous region of the full requested length. The idea is to map the same backing store twice sequentially in virtual memory. It leads to a much simpler implementation, because you don't have to handle the edge cases that relate to wrapping around. Sample implementation in C++17 here: https://github.com/stanford-stagecast/audio/blob/main/src/util/ring_buffer.hh https://github.com/stanford-stagecast/audio/blob/main/src/ut... https://github.com/stanford-stagecast/audio/blob/main/src/util/ring_buffer.cc https://github.com/stanford-stagecast/audio/blob/main/src/ut...
- ncmncm 5y agoThat is a very good idea. You don't even need to double-map memory. Just make the ring buffer smaller than your actual buffer, and treat any overrun as if it had wrapped around, but really just write past the (official) end. You might waste a bit of space at the front when you do, just to keep the code simple, but if you didn't have some room to waste, you wouldn't be using a ring buffer. Another way to simplify management is to give the ring buffer a power-of-two size, and make the head and tail counters 64 bits, masking off the high bits when actually looking at the buffer. They only ever increase.
- injinj 5y agoOne thing I found useful is instead of volatile T member; T access( void ) { return member; } Use volatile on the functions instead: T member; T access( void ) volatile { return member; } This has causes all access to this-> within the function to be volatile, including read_position, write_position, overrun_flag and data[]. When inlining these functions within other parts of the code, the compiler can hoist reads into registers from the non-volatile members unless you add the compiler barrier that @ghhhhhk8899jj mentioned.
- mhogomchungu 5y agoDon't use "NULL" in C++[1]. [1] https://cpp4arduino.com/2018/10/26/why-cpp-programmers-dont-use-null.html https://cpp4arduino.com/2018/10/26/why-cpp-programmers-dont-...
- dataflow 5y agoEh. It's fine in my experience... of all the things I've gotten bitten by in C++, this has not been one of them. It's actually a bit more readable IMO... nullptr doesn't scream "null" like it should. And it's best not to overload int with pointers anyway, because other people using your code may still use NULL even if you personally don't. The one thing to watch out for is usage in template call sites, where you'd want to cast it to the correct type first, but at that point you'd want to cast the nullptr too.
- imron 5y agoFrom the article: > Are there any drawbacks to using nullptr instead of NULL? No, unless you target old compilers that don’t support C++11, which is very unlikely. and OP is specifically targeting c++98.
- gpderetta 5y agoWhat else then? In c++11 you would of course use nullptr_t, but the OP code is pre-c++11. GCC used to use a magic builtin in pre C++11 to implement NULL, but IIRC they removed it as non-conforming.
- deleted 5y ago[deleted]
- john_fushi 5y ago> memset(data, 0, LENGTH); >// ... > T data[LENGTH]; I'm not sure how important it is in practice, but I'm pretty sure you don't zero out the whole array for sizeof(T) > 1. Anyhow, memsetting to 0 a complex type is... not something I'd recommend in most cases.
- dataflow 5y agoOuch, yeah. They need std::fill(). This is the kind of thing that makes you lose faith in a C++ library. (Also, what's with the volatile private variables?!)
- klodolph 5y agoThe use of volatile is typical here. It allows the ring buffer to be used from interrupts, as long as you have one reader and one writer at a time. I haven't checked the code for correctness, but in a typical ring buffer implementation intended to be used in interrupts, you would make the read and write pos volatile. To write, you put the value in the array, and then advance the write position. To read, you copy a value out, and then advance the read position. Volatile ensures that if a read is interrupted by a write or vice versa, the entire operation is still atomic. Without volatile, the compiler has more freedom to reorder memory access.
- ncmncm 5y agoThe ring buffer is the tool of champions. But confining yourself to C++98 is just masochism. Just because we're embedded doesn't mean we can't have nice things.
- R0b0t1 5y agoRight, I'm using the most recent C++ standard via GCC on STM32 parts now.
- ghhhhhk8899jj 5y agovolatile does not imply compiler fence on gcc, so this code has a race condition when updating read/write_position, you need compiler barriers here >data[write_position] = value; >COMPILER_BARRIER(); // __asm__ volatile("":::"memory"); >write_position = (write_position + 1U) % LENGTH; and same in Skip()
- gpderetta 5y agoexactly. The fence is required even when communicating with a signal handler (or interrupt handler) on the same thread. For for the multithreaded case, depending on the architecture, the compiler barrier is likely not sufficient and an actual hardware #StoreStore fence might be required. A specular barrier is needed on the reader side.