-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add queue static allocation support #11342
add queue static allocation support #11342
Conversation
@maciejbocianski, thank you for your changes. |
57a59c0
to
b61ecec
Compare
This looks pretty sweet.
(Not a huge fan of those being members of But on the other hand, such a construction wouldn't be
) So maybe leave those out for now. |
a5036e7
to
f6ce710
Compare
|
events/source/equeue.c
Outdated
@@ -21,6 +21,12 @@ | |||
#include <stdint.h> | |||
#include <string.h> | |||
|
|||
// check if the event is allocaded by user - event address is outside queues internal buffer address range | |||
#define IS_USER_ALLOCATED_EVENT(e) (((unsigned int)(e) < (unsigned int)q->buffer) || ((unsigned int)(e) > ((unsigned int)q->slab.data))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Casting to integer makes it probably safer than comparing pointers (it's undefined behaviour to compare pointers not inside the same object), but the integers should be uintptr_t
, or this could go nasty on 64-bit platforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed unsigned int
-> uintptr_t
events/source/equeue.c
Outdated
// check if the event is allocaded by user - event address is outside queues internal buffer address range | ||
#define IS_USER_ALLOCATED_EVENT(e) (((unsigned int)(e) < (unsigned int)q->buffer) || ((unsigned int)(e) > ((unsigned int)q->slab.data))) | ||
// for user allocated events set id as its address with first bit set | ||
#define MAKE_USER_ALLOCATED_EVENT_ID(e) ((int)(((unsigned int)e) | 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this is potentially a bit icky when we need to squeeze a pointer into the int
, We probably get away with it for now, but at some point might need to think about making the int
be intptr_t
, or actually storing an ID independently inside the object rather than mapping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or extend equeue
API by void equeue_cancel_by_ptr(equeue_t *queue, struct equeue_event *e);
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes, indeed. No point using ID numbers with user-allocated events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this force changing UserAllocatedEvent
API to not provide event id to outside world.
Only UserAllocatedEvent ::cancel
will work and EventQueue::cancel
will not.
I'm not sure how similar the Event
and UserAllocatedEvent
APIs should be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EventQueue::cancel
could have an overload accepting a UserAllocatedEvent *
.
I don't think there's any fundamental reason for them to mirror each other that degree. For a user-allocated event, an ID isn't fundamentally useful. And the post cannot fail anyway, so making any posts be void
-returning enforces that more than just saying "this won't return 0".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reached the same conclusion and separated the API into 1. simple call functions, 2. dynamic event functions (void*/ids), 3. static event functions.
Canceling static events with an id also has a performance issue (see review comment).
In the changes I came up with, post returned EBUSY if the static event was coalesced, though I don't know if this is necessary.
events/source/equeue.c
Outdated
return 0; | ||
struct equeue_event *e = 0; | ||
if (IS_USER_ALLOCATED_EVENT_ID(id)) { | ||
equeue_mutex_lock(&q->queuelock); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could pull this lock outside the if, just to minimise code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You want the lock to contain the minimum amount of code though. It's a critical section, so every cycle counts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I certainly agree with the general principle, but not convinced putting a single if (id & 1)
inside a critical section is enough extra overhead to be worth increasing code size to avoid.
The critical section would be better served by eliminating the list search by removing the use of IDs for static events, as suggested previously.
f6ce710
to
6703a46
Compare
Ah! First glance this looks like a good solution to static events if you must maintain backwards compatibility. Downside is you're limiting yourself hardware-wise. Likely a great solution for Mbed OS though. Will review. Just an FYI, I've been working on something similar, but with reversed requirements: Portable but with backwards compatibility not a concern. It's unfortunate GitHub really doesn't have a way to collaborate before PR: (FYI @kjbracey-arm that branch also has a priority mechanism, event coalescing, and standardization on POSIX-esque error codes) |
events/source/equeue.c
Outdated
#define IS_USER_ALLOCATED_EVENT(e) (((uintptr_t)(e) < (uintptr_t)q->buffer) || ((uintptr_t)(e) > ((uintptr_t)q->slab.data))) | ||
// for user allocated events set id as its address with first bit set | ||
#define MAKE_USER_ALLOCATED_EVENT_ID(e) ((int)(((uintptr_t)e) | 1)) | ||
#define IS_USER_ALLOCATED_EVENT_ID(id) (((id) & 1) == 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Would suggest prefixing these with EQUEUE_
for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
events/source/equeue.c
Outdated
return 0; | ||
struct equeue_event *e = 0; | ||
if (IS_USER_ALLOCATED_EVENT_ID(id)) { | ||
equeue_mutex_lock(&q->queuelock); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You want the lock to contain the minimum amount of code though. It's a critical section, so every cycle counts.
events/source/equeue.c
Outdated
if (IS_USER_ALLOCATED_EVENT_ID(id)) { | ||
equeue_mutex_lock(&q->queuelock); | ||
struct equeue_event *cur = q->queue; | ||
while (cur) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work if static events are set to go off at the same tick? equeue uses a 2d linked-list so the events would become siblings. I believe you need to follow the sibling pointers as well.
Something to be concerned about: this is a O(n) loop in a critical section. Currently equeue promises a O(1*ticks) in critical sections. Not a deal breaker, just worth noting.
My conclusion was that a pointer-based cancel function and erroring on traditional cancel with a static event was the correct solution for performance and usability reasons. Any user with a static event will have its address ready to go anyways, so that's one less pointer for them to store.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch
I fixed it and updated tests to cover this
events/source/equeue.c
Outdated
} | ||
|
||
|
||
// equeue scheduling functions | ||
static int equeue_enqueue(equeue_t *q, struct equeue_event *e, unsigned tick) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth asking: What happens if you post the same static event twice? I believe the current code explodes as equeue tries to append the event as its own sibling? Not really sure what happens.
@flit made the good point that static events should coalesce in this case. This makes static events a nice alternative to binary semaphores/eventflags if they aren't available on your system. (and maybe helps with all the corner cases around tear down of event heavy classes @kjbracey-arm?).
This logic takes care of coalescing:
https://github.com/geky/equeue/blob/e50bf24cc82874b694e643e521498ed357647680/equeue.c#L216-L221
(The coalesce flag is because that implementation has two separate post entry points for dynamic and static events, it could be modified to use your IS_USER_ALLOCATED_EVENT macro).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a problem but we assumed that such situation will not happen, but API allow this.
@kjbracey-arm maybe to add more safety to UserAllocatedEvent
we should leave only bool try_call
API to prevent double posting ? It will also ease canceling, only UserAllocatedEvent::cancel
will work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel strongly that it should be possible to safely make a second queue call and have it ignored via some API, so there's no need for separate "have I queued" tracking logic in the client - a sigio handler can just do post()
.
Maybe this usage should be distinguished via try_post
(return bool/error) versus post
(return void, assert on fail).
The other plausible behaviour (which might be preferable when we can pass parameters) would be to actually have a second queue attempt replace the first one (updating parameters, but retaining position in queue). That's indistinguishable if you're passing no parameters.
83f35be
to
f74a36b
Compare
fixed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have time to review completely the PR today however I skimmed through it and I'm concerned by the lack of extensibility if an application uses its own allocators and allocate events with them. Wouldn't it be possible to use equeue_event::dtor
as a function pointer that release all resources associated with the event; not just destroying the event. Just like a smart pointer destructor it would destroy the pointed object and release it. It would also prevent pointer tagging.
@maciejbocianski @kjbracey-arm @geky What do you think ?
@kjbracey-arm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd really like to have a mode that wouldn't pull any dynamic memory in at any stage. Also could you update the doxygen of equeue and even queue to mention the "static" mode and how to use it.
events/UserAllocatedEvent.h
Outdated
* { | ||
* // queue with not internal storage for dynamic events | ||
* // accepts only user allocated events | ||
* EventQueue queue(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That still requires malloc (even if of 1). Could we have a special mode that doesn't pull the malloc in at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you should be able to just do queue(0)
if intending only to use static events.
(With current code, you could do queue(1, &my_single_char)
to avoid the malloc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or do it this way
EventQueue::EventQueue(unsigned event_size, unsigned char *event_pointer)
{
+ if (event_size == 0) {
+ static uint8_t dummy;
+ equeue_create_inplace(&_equeue, event_size, &dummy);
+ } else {
if (!event_pointer) {
equeue_create(&_equeue, event_size);
} else {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, assuming it never touches dummy
. And if it REALLY doesn't ever touch dummy, you could, in principle just pass &_equeue
as your buffer instead, saving 1 byte of RAM (in all images - that static dummy gets reserved whatever).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I encountered that doing it via static uint8_t dummy;
cause some strange run time errors.
e.g. this assert
mbed-os/platform/SingletonPtr.h
Line 139 in 98c0fd0
MBED_ASSERT(p == reinterpret_cast<T *>(&_data)); |
So for now dummy is regular EventQueue member.
I think that 1B more per queue is not much.
How many queues utilize typical program?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds dangerous. As that assert notes, if it's triggered, someone has corrupted memory. Which suggests that your attempt to do that has made it run off the end of _dummy
. It may still be doing that, with the dummy elsewhere, and you're not noticing.
Is there some assumption that the buffer is at least some size, and it's writing in a fixed header?
You could catch that with a watchpoint on _dummy + 1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed. The problem was in queue buffer alignment
int equeue_create_inplace(equeue_t *q, size_t size, void *buffer)
{
// setup queue around provided buffer
// ensure buffer and size are aligned
if (size >= sizeof(void *)) {
q->buffer = (void *)(((uintptr_t) buffer + sizeof(void *) -1) & ~(sizeof(void *) -1));
size -= (char *) q->buffer - (char *) buffer;
size &= ~(sizeof(void *) -1);
} else {
// don't align when size less then pointer size
// e.g. static queue (size == 1)
q->buffer = buffer;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, but why is it accessing the buffer at all, even after alignment?
If you were to do that, then for symmetry you'd kind of want I'm more concerned about maintaining constant initialisation for
But it's still easy to fail in practice - it wouldn't work with The upshot of all that is that it will generally be easier to construct your events unbound, and bind them later. There should at least be an API to do just a bind during an init, so all posts can just |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, looks fine to me, but change the in-place to 0, nullptr if you can. (If it passes CI like this, you can leave it).
EventQueue::EventQueue(unsigned event_size, unsigned char *event_pointer)
{
if (event_size == 0) {
equeue_create_inplace(&_equeue, 0, NULL);
} else { Setting 0 size looks OK // check if the event is allocaded by user - event address is outside queues internal buffer address range
#define EQUEUE_IS_USER_ALLOCATED_EVENT(e) (((uintptr_t)(e) < (uintptr_t)q->buffer) || ((uintptr_t)(e) > ((uintptr_t)q->slab.data))) But this also should be OK, will test it |
Hmm, skirting deeper into undefined behaviour territory I guess. But as it's not null pointer CONSTANT, only a null pointer, I can't see it causing extra issues. (Except LTO?) You could add a leading |
Test run: FAILEDSummary: 1 of 4 test jobs failed Failed test jobs:
|
CI restarted |
Fails looks not related |
@kjbracey-arm EventQueue::EventQueue(unsigned event_size, unsigned char *event_pointer)
{
if (event_size == 0) {
// As static queue (EventQueue(0)) won't perform any access to its data buffer
// we can pass (0, NULL)
equeue_create_inplace(&_equeue, 0, NULL);
} else { |
equeue_create_inplace(&_equeue, 1, this); | ||
// As static queue (EventQueue(0)) won't perform any access to its data buffer | ||
// we can pass (0, NULL) | ||
equeue_create_inplace(&_equeue, 0, NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general should be shifting to nullptr
in C++ code, but doesn't matter when calling C code anyway.
UserAllocatedEvent provides mechanism for event posting and dispatching without utilization of queue internal memory. UserAllocatedEvent embeds all underlying event data and doesn't require any memory allocation while posting and dispatching. All of these makes it cannot fail due to memory exhaustion while posting.
without this fix test_equeue_break_no_windup was failing on IAR
e7a953d
to
4c3889d
Compare
Test run: FAILEDSummary: 3 of 4 test jobs failed Failed test jobs:
|
Aborted, restarting |
Started CI after astyle fix |
Test run: SUCCESSSummary: 11 of 11 test jobs passed |
Another CI needs to finish after astyle fix |
Test run: SUCCESSSummary: 11 of 11 test jobs passed |
…tic_alloc add queue static allocation support
Description
This PR implements mechanism to allow static event posting and dispatching.
UserAllocatedEvent
provides mechanism for event posting and dispatching without utilization of queue internal memory.UserAllocatedEvent
embeds all underlying event data and doesn't require any memory allocation while posting and dispatching. All of these makes it cannot fail due to memory exhaustion while posting.Docs update will be provided in separate PR
UserAllocatedEvent
implementation#9172
Pull request type
Reviewers
@kjbracey-arm
@pan-
@jamesbeyond
@geky
@bulislaw
Release Notes
Adds UserAllocatedEvent API to provide mechanism for event posting and dispatching without utilization of queue internal memory.