We are working on an implementation of defer statements that makes most defers no more expensive than open-coding the deferred call, hence eliminating the incentives to avoid using this language to its fullest extent. We wanted to document the design in the proposal/design repo.
See issue #14939 for discussion of defer performance issues.
The design document is posted here. The current, mostly complete implementation is here
Comments are welcome on either the design or the implementation.
Change https://golang.org/cl/196964 mentions this issue: design: add 34481-opencoded-defers.md
Change https://golang.org/cl/197017 mentions this issue: misc, runtime, test: extra tests and benchmarks for defer
@danscales, should this be milestoned to 1.14, or is it a longer-term effort?
Yes, Go1.14 milestone is correct -- that's what we're aiming for.
This proposal only allows up do 64 defers per function, is that correct?
I imagine this is enough for most hand written code, but I can imagine some frameworks generating a lot more than that...
Or imagine test code opening every file in a directory and deferring the file close. This won鈥檛 work for large directories and may break backwards compatibility in some cases. Also, what would be the error?
This proposal only allows up do 64 defers per function, is that correct?
Yes, there is some limit to the number of defers per function (the size of the deferBits bitmask). In the current implementation, it is 8, but easily changed to 64.
But to clarify, if open-coded (inlined) defers cannot be used in a function, we just revert back to the current way of doing defers (create a "defer object" and add to the defer chain, process the appropriate objects on the defer chain at all function exits).
Our goal is to reduce the defer overhead in the common cases where it occurs, which is typically smallish functions with a few defers. In unusual cases (functions with defers in loops or lots of defer statements), we just revert to the current method.
I will add some text to the design document to make this clearer.
Is there a way to avoid checking deferBits for defers that are always executed? I don鈥檛 mind that the respective bits get set, but there is no compiler optimization that鈥檚 able to remove checks for those bits (unless they are all always set and thus the variable is constant). It looks like it should be sufficient to check whether the defer is called in a block that dominates all exits (or something like that) and not emit the bitcheck in that case.
@rasky dominating all exits is not enough; consider:
func foo() {
bar()
defer baz()
// ...
}
If bar() panics, baz() should not be executed (even though the defer dominates all exits).
The special panic handler can still check the bits if it needs to. My comment is about not generating bit checks in open-coded defers within the function body.
Is there a way to avoid checking deferBits for defers that are always executed? I don鈥檛 mind that the respective bits get set, but there is no compiler optimization that鈥檚 able to remove checks for those bits (unless they are all always set and thus the variable is constant). It looks like it should be sufficient to check whether the defer is called in a block that dominates all exits (or something like that) and not emit the bitcheck in that case.
Yes, the deferBits conditionals (in the inline code) are eliminated completely in the simple cases where all defers are unconditional. If all defers are unconditional, then the stream of operations on deferBits are all unconditional and just involve constants, so the value is known everywhere through constant propagation. The 'opt' phase of the compiler does the constant propagation and then eliminates the if-check on deferBits which is always true.
As you point out (good point!), if one defer is always executed (dominates the exit) but others are not, we could potentially remove the deferBits check for that specific defer on any exits after that defer. I will put that on the TODO list (and add that as an idea for the next rev of the design doc). I may not do that optimization for the initial checkin.
Wasn't @dr2chase asking about bitwise constant-propagation a while back?
@danscales inlining happens before defers are lowered in your implementation, so in the following example Unlock() is not inlined in foo(), correct? 馃槶
func foo(m *sync.Mutex) {
m.Lock()
defer m.Unlock()
// ...
}
Yes, inlining happens much earlier in the compiler, so for my implementation, even though the defer calls will be directly expressed (open-coded) at exit, the calls cannot be replaced by their definition (i.e. inlined).
Change https://golang.org/cl/190098 mentions this issue: cmd/compile, cmd/link, runtime: make defers low-cost through inline code and extra funcdata
I'm not too familiar with the internals of the compiler, but would it be possible to mix unconditional, conditional, and looped defers in the same function with this design? For example, insert hard-coded calls to any unconditional defers, regardless of others, at the exits, _and_ insert the bit checks for conditional ones, _and_ insert code to deal with looped defers?
In other words,
func Example(t int) {
defer Unconditional()
if t > 3 {
defer Conditional()
}
defer AlsoUnconditional()
for i := 0; i < t; i++ {
defer Looped(i)
}
}
would essentially have the equivalent of all of the following inserted at the returns:
for i := len(deferredLoopedCalls) - 1; i >= 0; i-- {
deferredLoopedCalls[i]()
}
AlsoUnconditional()
if conditionalBitSet {
Conditional()
}
Unconditional()
We already handle conditional and non-conditional defers with this proposal. You have to set the bitmask in all cases, since you can have panics (especially run-time panics) at many points in the function, and so it is not easy to know otherwise (without a detailed pc map) whether you hit a particular unconditional defer statement before the panic happened.
For actual exit code, you can eliminate some bitmask checks if a particular defer statement dominates the exit that you are generating for. That optimization is not in this change, but seems reasonable to do soon as an optimization - definitely on my TODO list.
It would seem to be fairly complex to combine the low-cost defers with the looped defers (which would have to be handled by the current defer record approach) in the same function. I think a bunch more information would have to be save and new code generated to make sure that the looped defers and the inlined defers were executed in right order. So, I'm not planning to do that (especially since those cases are very rarely and not as likely to be performance-sensitive).
Change https://golang.org/cl/202340 mentions this issue: cmd/compile, cmd/link, runtime: make defers low-cost through inline code and extra funcdata
Fixed by be64a19
Could a defer propagate an error value through the function's normal return values? Defers are most often used for closing I/O handles and the like, which often have important error information.
Workarounds:
@mcandre I'm sorry, I don't understand what you are asking. In any case, this does not seem like the right place to ask it. This issue is about a specific design of the implementation of defer. It's not about how defer is defined to work in the language. You may want to use a forum; see https://golang.org/wiki/Questions. Thanks.
Most helpful comment
Yes, the deferBits conditionals (in the inline code) are eliminated completely in the simple cases where all defers are unconditional. If all defers are unconditional, then the stream of operations on deferBits are all unconditional and just involve constants, so the value is known everywhere through constant propagation. The 'opt' phase of the compiler does the constant propagation and then eliminates the if-check on deferBits which is always true.
As you point out (good point!), if one defer is always executed (dominates the exit) but others are not, we could potentially remove the deferBits check for that specific defer on any exits after that defer. I will put that on the TODO list (and add that as an idea for the next rev of the design doc). I may not do that optimization for the initial checkin.