Zinger
https://codeberg.org/vincent-dalstra/zinger
Zero-copy ringbuffer, intended for efficient message-passing between threads on multi-core microcontrollers, such as the ESP32-S3.
Started as a port of this C library in the esp-idf: FreeRTOS (Supplemental Features) - ESP32-S3 - — ESP-IDF Programming Guide v6.0.2 documentation
Essentially, it lends out slices of its backing buffer - a bit like an allocator.
-
sendAcquire()allocates a slice of memory, which the producer can write to directly. -
sendComplete()passes ownership of the slice back to the ringbuffer. -
receiveAcquire()returns the same slice of memory, which the consumer can read from directly. -
receiveComplete()free’s the memory so it can be re-used.
Producer/consumer see the same slice of memory, so there is no need to @memcpy inside the library. This in-turn means the critical section is short, and multiple producers/consumers can work on their borrowed slice at the same time.
Note on performance
This is probably not performant on ‘real computers’, due to how L1/L2 caches work…
Microcontrollers like the ESP32-S3 do not have per-core memory caches. Each CPU core will load/store directly from its registers to system memory - 512 kB of on-die SRAM. Synchronisation is necessary only when accessing the same bytes.
On a modern x86 computer, the per-CPU L1 cache (and sometimes L2) means threads that read/write to adjacent slices would invalidate each other’s cache, resulting in lots of RFO messages and reduced performance because the cache is not able to do its job.
That’s assuming I understand that all correctly; I mostly work on embedded systems.
Things learned:
-
What a Futex is, and how to use it.
-
I did first try using the higher-level synchronisation primitives such as
ConditionandSemaphore, but wasn’t able to make my tests pass with them. Futex was easier. -
Futex’ are weird.
-
Slices are great. So many patterns in (good) C code take or return a pointer and a size field. Having a data type, and all the language support and ‘implicit asserts’ makes it so much nicer to work with.
-
Concurrency != Paralellism. You can make a structure thread-safe with just Atomics (spin-lock), no Io needed. Conversely, concurrency is not limited to the number of cores on your CPU.
-
Serialising/deserialising a struct with slices in it is a pain (done in the ‘grep’ example). Perhaps I should make a helper library for it…
Supported Zig versions
zig-0.16.0
Where is it used?
Nowhere. Maybe in the future, when I get around to compiling zig for the esp32…
By way of example, I am using the original c library it’s based on in production code, to improve the performance of the ‘log_router’ component, a library for the esp-idf that saves copies of system log messages (e.g. ESP_LOGW(), equivalent to std.log.warn() to file(s) on disk (either SPI flash, or SD-card). Unfortunately, the way it was written meant it would sometimes do file-IO during the log call, with all the latency problems that brings.
A zero-copy ringbuffer is perfect to solve this. vsprintf() can write directly into the buffer, so there’s no redundant copy. Then, when either of the two cores has nothing better to do, they can write out buffered messages to the log files.