-
-
Notifications
You must be signed in to change notification settings - Fork 63
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
Make "replace" the default policy for scheduling actions in the C target #1464
Comments
This is a very good point. I would support switching to a last writer wins semantics. There are some subtleties, though:
|
I would want to require that only
It sounds like you are assuming that we would care about whether |
Sounds rather like the "replace" policy described here: https://www.lf-lang.org/docs/handbook/actions?target=c#action-declaration
|
I would like to add to the discussion that the semantics described by @petervdonovan are exactly the ones implemented in the C++ target. There is an old issue stating that the policy in C++ should be updated to the one used in C: #236. Also note the discussions in this issue (they diverge in a different direction though). The issue is not fixed yet for two reasons. First, I personally was never really convinced that the current C semantics is a sane default and, second, I did not yet encounter a use-case where it actually mattered. I would be all for using the replace policy described here as default. It seems more intuitive and can be easily predicted. By this I mean that if I call schedule at tag (t, n)/with delay d, then I know precisely hat the new event is scheduled at tag (t+d, 0). With the current C policy, it could be any tag (t+d, m) and I cannot make any predictions on the value of m as I don't know which other reactions might already have scheduled the action at t+d. While writing this I also thought of an actual use-case that is not possible to implement using the current C semantics. Imagine if we would want to "unschedule" an action. I think there is an issue somewhere saying that it would be a great feature to be able to actually delete events from the event queue. Here, however, I mean overwriting an events value (e.g. with nullptr) and thus marking it as invalid to the reactions triggered by it. |
I'll try to summarize what I understood from what Edward has said and from my conversation with @lhstrh. It seems like we have three options on the table for how to assign microsteps to events/messages scheduled into the future: elementwise addition of logical times (also discussed in the issue that Christian linked I think I've underappreciated the usefulness of queueing since of the three, it seems the most connected to the established work (which I should spend more time reading). Here are some observations that might affect how the three options might be evaluated:
* "No algebraic structure" in this context means that the time at which an event is scheduled does not just depend on the current logical time and the delay with which the event is scheduled. It also depends on the state of the event queue. This dependency on global state makes it complex to analyze in the same ways as the other two approaches. |
I'm having trouble figuring out what the three options that you mention actually are. Can you clarify? Issue #203 does not seem relevant. |
Oops -- fixed.
Suppose a reaction executes at tag
If a reaction instead schedules an event |
I am convinced we should do element-wise addition. Volunteer to fix it in the C target? I'm not so sure whether we should do queueing or replacement when there is a collision. I guess the policies we have actions could specify these. |
Okay, that sounds reasonable. If we do elementwise addition then that would mean going "all in" on super dense time, in which case I would argue that queueing in the microstep dimension would defeat the purpose (of microstep predictability). Just to play devil's advocate, elementwise addition (even with the "drop" or "replace" policy which I like) does have downsides that the weird addition (and maybe queueing) do not have:
|
Ok, I think the proposal on the table is this:
I like the simplicity of this. It means we could remove the replacement policy from the LF syntax. If we really need it (I don't have use cases), then we could provide a runtime API function get_event(action, time, microstep) that returns NULL if there is no event on the event queue with the specified tag and the event otherwise. Note that with physical actions, this simpler policy does not incur a risk of missing events because the physical clock is strictly increasing (at least in the C target). Should we go ahead with this? |
I'm confused about the very first post in this thread. In the example: reaction(startup) -> a, b {=
// Based on this reaction body, it looks like a and b will be logically simultaneous,
// but in fact a will be processed a microstep later. The user must know the state
// of the event queue in order to be aware of this misalignment.
lf_schedule_int(a, 0, 1);
lf_schedule_int(b, 0, 1);
=} ... it is stated that "it looks like a and b will be logically simultaneous, but in fact a will be processed a microstep later". Why is that? I would think that since |
@petervdonovan, @edwardalee For me it is difficult to understand why you prefer the "elementwise addition" over the "weird addition". I have argued against the "queueing" in the past and I am fully on board with dropping it. However, to me the "elementwise addition" seems weird and I still find the "weird addition" (as you call it), most intuitive. To me it seems really strange to require that microsteps are monotonic. I will try to explain below why.
In summary, I think there is a lot that we would loose by using the "elementwise addition", but I could not extract from the discussion above what we would actually gain (other than it is mathematically nicer to express). Could you maybe explain the reasoning behind this and the practical impact of using elementwise addition? |
Yes, Christian and I seem to be on the same page about items 3 and 4, and items 2 and 5 both seem like examples where you sort of diverge along different subspaces (span of (1, 4) vs span of (1, 5), and span of (1, 0) vs. something else, respectively), which I agree might not be good. I think a benefit of elementwise addition that is not merely aesthetic is that fewer events get dropped, since if you schedule something with the same delay into the future from two different microsteps, then they will not collide with each other. But I am not so sure that this is so beneficial, because it will still be possible to drop events in other cases, and because in many cases it is desirable to drop events (in a predictable way) to avoid overutilization. To Christian's item 1, I agree, it is possible to imagine that it would be most intuitive for microsteps to start at zero. In the benchmarks, and perhaps even in non-pathological LF programs, it might be useful to support iterative programs. To do that you must iterate in the microstep dimension; in that case, the microstep is like the loop variable in a |
Is the potential dropping of events a real problem? I think it is save to say that we have tested the "weired addition" for 2 years now, and so far I have not encountered a practical example where the dropping of events happened unintentionally. As described elsewhere, I actually consider the dropping a feature that allows to modify and/or "unschedule" events (by overwriting them with an invalid value). It seams to me like the proposed solution solves a rather academic problem at a relatively high cost. |
Regarding item 1, the use of microsteps in languages like VHDL, I see this as much more like our "levels" than like our microsteps. And indeed, levels do reset to zero at every stage of time advance. The Newton's cradle example, however, is a good one and indeed it could be the killer example that justifies the "weird addition." I would be much more comfortable seeing what happens when you actually build an LF model of this. Is there a way to avoid the divergence of microsteps even without the weird addition? I doubt it. Any volunteer to reduces this example to a concrete LF test case? |
Here's an attempt at summarizing what we talked about during our meeting.
Existing syntax: The idea is that when an action is "deferrable", either this directly implies use of the "defer" policy (equivalent with the old syntax) or it means that the programmer may choose to override the default "replace" behavior and defer on a case-by-case basis for each call to |
Regarding the question of how to expose the different policies (defer/drop/replace): I still think they might best be implemented in standard library reactors, because I think this approach provides
Here is an example: target C { Build-Type: Debug }
/**
* Funnel messages from many channels into a single channel using the microstep dimension.
*/
reactor MessageFunnel(fan_in: int(2), buffer_size: int(20)) {
preamble {=
// FIXME: Must be kept in sync with buffer_size
#define BUFFER_SIZE 20
typedef int buffer[BUFFER_SIZE];
=}
input[fan_in] in: int
output out: int
state pending: buffer // Hardcoded buffer size :(
state queue_start: int(0)
state size: int
logical action try_again
initial mode receiving {
reaction(in) -> out, reset(emptying_buffer) {=
int i = 0;
while (i < self->fan_in) {
if (in[i]->is_present) {
lf_set(out, in[i++]->value);
break;
}
i++;
}
if (enqueue_inputs(in, i, self->fan_in)) lf_set_mode(emptying_buffer);
=}
}
mode emptying_buffer {
logical action t
reaction(reset, t) -> t {= lf_schedule(t, 0); =}
reaction(in) {=
enqueue_inputs(in, 0, self->fan_in);
=}
reaction(reset, t) -> out, reset(receiving) {=
lf_set(out, self->pending[self->queue_start++]);
self->queue_start %= self->buffer_size;
self->size--;
if (!self->size) lf_set_mode(receiving);
=}
}
method enqueue_inputs(inputs: messagefunnel_in_t**, start: int, end: int): bool {=
bool enqueued;
for (int i = start; i < end; i++) {
if (inputs[i]->is_present) {
enqueued = true;
enqueue(inputs[i]->value);
}
}
return enqueued;
=}
method enqueue(value: int) {=
if (self->size == self->buffer_size) {
lf_print_error_and_exit("Buffer overflow in MessageFunnel.");
}
self->pending[(self->queue_start + self->size++) % self->buffer_size] = value;
=}
}
reactor Stdout {
input in: int
reaction (in) {=
lf_print("%d", in->value);
=}
}
reactor Count(bank_index: int(0), stop: int(3), step: int(1)) {
output out: int
initial mode active {
logical action a
state count: int(bank_index)
reaction(startup, a) -> a {= lf_schedule(a, 0); =}
reaction(a) -> out, reset(dead) {=
lf_print("Sending %d", self->count);
lf_set(out, self->count);
self->count += self->step;
if (self->count >= self->stop) lf_set_mode(dead);
=}
}
mode dead { /* GC ME! */ }
}
main reactor {
counts = new[2] Count(stop(10), step(2))
funnel = new MessageFunnel(fan_in(2))
stdout = new Stdout()
counts.out -> funnel.in
funnel.out -> stdout.in
} Here is the output:
|
Nice! This could be generalized using tokens and the vector object you created so that the same code could be used for any data type. However, we have no support for yet for polymorphic types in the C target. I've been thinking that we could have |
Another approach would be to use macros, like
Doing it this way increases code size because for every type that the generic reactor is instantiated with, you must redefine the generic type I believe that under the hood, C++ uses a macro-style approach like this one while Java's "boxing" is more like this "wrap in a token" idea, and I have heard that C++ and Java faced the considerations that I described here. I should elaborate on why the claim of efficiency and type checking is relevant here since they only seem to matter if one actually does operations on values that have the generic type. Such operations could be provided as macros or as function pointers. Besides, if you are storing values of a generic type in an array without necessarily doing any operations on them, it might help to know how many bits they have so that you don't have to treat everything as a pointer. |
We now do have generics in the C target. Maybe we should revisit @petervdonovan's component-based suggestion vis-a-vis the "policy" argument of actions that we currently have. I agree that building a standard library of reactors sounds attractive. We are inching closer to a first release of |
This discussion has diverged from its original purpose somewhat, and I have not thought about this for a while. However, I think I agree that replacing these special syntax features with library reactors seems like a reasonable thing to put on the roadmap for the long term. But in the very near term I am not sure I am ready for the bikeshedding that any syntax removals would likely entail. |
Reading this thread, however, it does seems that everybody agrees that "replace" should be the default (and not "defer"). |
I would like to ask for some clarification on this issue. The original issue to my understanding was scheduling an action at the same time but a different tag and how that should be handled. IMO If replace as a whole and not at the same logical time is the case I feel like devs may be a bit blindsided by dropping data for queued events. I believe that dropping data should be something the developer has to explicitly say to do. It is easier to see something is running slow then on why certain actions may not trigger all the time depending on how the drop happens. Another thought for this is that if the default policy could change while still keeping the syntax the same then would it be appropriate to add a warning for when a dev does not define explicitly what policy is being used to say the current policy and suggest they set an explicit policy? |
To be clear, in this thread, "replace" is (mostly) used to talk about what happens when two calls to I think the consensus above is about what to do when the future tags are identical, rather than what to do when they are too closely spaced. @lhstrh suggests that the consensus in this case is to replace, as we do when a reactor writes to an output port twice at the same tag. I think this answer is defensible, but it is not what the C target currently does. It is not a high priority for me to change the current behavior since it is fairly easy for a programmer to avoid this situation. When a What I've implemented in much simpler and will prove useful if An alternative might be for the event at time |
My impression is that we might want to give users better feedback and control over the policy. I think @SheaFixstars has a good point about blindsiding devs by just dropping data. Maybe it would be a good idea to make the default policy drop and have |
This is not so much a bug report as a question about whether the semantics as currently designed is what we really want.
We currently allow scheduled actions to pile up in the microstep dimension instead of having a "last write wins" policy like what we have for ports. The question is about whether this asymmetry between ports and actions is really necessary.
The purpose of microsteps might be
So, how useful is use case 1? Can we dispense with it?
Here is why I ask. The piling up of events in the microstep dimension can make it difficult to predict how different scheduled actions will align with each other. The logical time of the scheduled event now depends not only on the current time, the min delay, and the additional delay, but the state of the event queue as well. Furthermore, an approach to determining maximum rates at which logical actions are present that is based on the set of times when they can be present becomes more complicated: If you do not know which/how many microsteps there are when an action can be present, the number of times the action can be present in a time interval is unbounded. I will try to follow up later with more details on the types of conclusions that you cannot draw because of this, but it is a work in progress...
Example program:
Actual output:
Alternative possible output:
The text was updated successfully, but these errors were encountered: