I am implementing an MPMC dequeue API. The current implementation style is to pass the output result as an output parameter. This of course is inconvenient, because it means that for the caller, the output location must be defined as var rather than const.
pub fn dequeue(self: @This(), out_item: *T) !void {
const slot: *Slot, const ticket: Sequence = ...
out_item.* = slot.item;
slot.sequence.store(ticket +% self.lapStep(), .release);
return;
}
If possible, I would like to return T directly. But doing so would add an extra copy, which is why I chose the previous approach.
pub fn dequeue(self: @This()) !T {
const slot: *Slot, const ticket: Sequence = ...
const out_item: T = slot.item;
slot.sequence.store(ticket +% self.lapStep(), .release);
return out_item;
}
But I found an evil implementation that can avoid this extra copy by abusing defer.
pub fn dequeue(self: @This()) !T {
const slot: *Slot, const ticket: Sequence = ...
defer slot.sequence.store(ticket +% self.lapStep(), .release);
return slot.item;
}
I believe that using defer in scenarios other than resource release is an abuse, but this usage scenario is too tempting. Perhaps I need some excuse to convince myself that this is a reasonable āresource releaseā.
Why couldnāt you remove the ādeferā from your last example ? It would work the same way ! Moreover zig can surely avoid the extra copy when doing a copy elision pass in itās optimizer. Itās a well researched and understood optimization.
Do you mean:
pub fn dequeue(self: @This()) !T {
const slot: *Slot, const ticket: Sequence = ...
slot.sequence.store(ticket +% self.lapStep(), .release);
return slot.item;
}
This is of course incorrect code, which means the guard was removed before the copy was made.
Unlike RVO, NRVO is not always guaranteed as an optimization, and I donāt want to rely on it.
Well I believe in your case the guard is a ressource you need to acquire and release properly. ādeferā was meant to remove the need for RAII, so whenever you would want to emulate some sort of ādo this when exiting the scopeā using defer is the right choice IMHO. C++ has scope guards for mutexes to do the same thing, in zig in we use ādeferā instead.
(I misread your post sorry for my first response)
1 Like
Thank you, I realized that RAII could be used here if possible, so defer should be allowed.
why would this be considered āabusingā? defer is for running code at the end of the scope, resource clean up being the most common use case does not preclude other use cases. Your use case here is a perfectly fine usage of defer.
For your 2nd snippet, Iām pretty sure zig (or LLVM) will optimize out the copy, check the generated code just to be sure.
4 Likes
I used to think so, but #23734 made me realize that the core team seems to have somewhat different views on this.
itās essentially just intended for resource cleanup. Capturing errdefer encourages using the construct in more complex ways, which is usually not a good thing!
This means that logging based on defer is not a reasonable use case.
I am now also starting to reconsider whether using defer here is reasonable. My initial understanding was: if RAII advocates doing this, then using defer here would be reasonable. But I realized that RAII is also used for collecting logs, which the Zig team considers an abuse, so I think my use of defer here might not necessarily be considered acceptableā¦
I wouldnāt conflate defer and errdefer here, as defer is a general tool to run code unconditionally at the end of blocks. Using the Zig compiler as an example, defer is used for non-resource-release purposes such as incrementing indexes/offsets, shifting at intervals, reinitialization, seeking or skipping āto the next thingā, asserting post conditions, mark a batch as complete, and so on.
6 Likes
Itās possible the extra copy is optimized away by LLVM.
2 Likes