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
runtime: document finalizer semantics for tiny objects (<16 bytes) #46827
Comments
This seems like an issue with package log, not the compiler or runtime. See this variation on your test program: https://play.golang.org/p/e0fNmJ5YOZ7 It uses It's also not an escape analysis issue, because we don't currently do any optimizations to detect global variables that are never reassigned. I would check if package log has any slices where it buffers use data values, and maybe it needs to zero them out after use to avoid retaining objects. |
It does retain a bytes.Buffer, see #46285 |
@andig Sorry, I don't follow. Package log doesn't use bytes.Buffer, except in its unit tests. |
running with just |
@seankhliao Thanks, I see that too now. I forgot about the Go playground caching program output. |
Minimized test case: https://play.golang.org/p/SAXVXDVr-Ae It fails often, but not 100% reliably. However, it reliably passes if |
This seems like a GC issue, though I can't explain the sensitivity to importing "reflect". /cc @mknyszek |
FWIW, I tried it locally 50 times. It passed always. 🤷♂️ |
Sorry for the confusion, replied to quickly. Log has byte slice for assembling log output that it reuses. |
Interesting. Can you share any more details about your test system? I just double checked again with my system Go version, and it failed 320 out of 1000 runs.
Byte slices don't have pointers. Something would have to be really, really broken for a byte slice to prevent other objects from being garbage collected. Anyway, package log appears to have been a red herring. This issue can be reproduced without it. |
|
@agnivade Thanks. I just tried with my Go 1.17 branch checkout, and it's reliably passing for me too. So maybe we independently fixed this issue for Go 1.17 already. (My Go 1.17 checkout was in the middle of a messy rebase earlier, so I didn't trust it for testing.) If anyone has time/interest, it would great if someone could confirm that the test program fails for them with Go 1.16, and maybe do a bisect to identify what CL fixed it. |
On 1.16.4 linux/amd64, I see ~30% failures with the original reproducer, but never with #46827 (comment):
|
Hm, also interesting. Does it make a difference if you add the (Also, you might consider running |
It does not reproduce just with adding a blank import for log, but it sporadically does with a use of log: https://play.golang.org/p/TC4R7_VU6un |
The reproducer that uses log also fails occasionally on tip:
|
I see it both with 1.16.5 and tip with #46827 (comment)
|
Seems specific to Linux, as I can't reproduce this on Apple M1, both with go1.16 and gotip. |
Running with allocfreetrace and comparing a few good/bad runs might help identify the culprit. |
Aren't objects smaller than |
I haven't had a chance to look too closely, but as @CAFxX points out, any object not containing pointers that is < 16 bytes in size has its lifetime potentially shared with other allocations (they are allocated in a 16 byte block). Which allocations end up in the same block depends on so many factors (allocation order, which depends on goroutine scheduling, OS scheduling, the whims of hardware, etc.) that I wouldn't be surprised if an import causes some init function to run, which creates some tiny allocation that never dies, and now the finalized object is bound to it. To the letter, I think this behavior still agrees with |
Oof, right, I forgot about the tiny allocator. I agree that seems to explain everything seen, and the suggestion of bumping up the allocation size to at least 16 bytes should address the issue by bypassing the tiny allocator. |
Closing the issue because it seems like things are working as intended, and there's no obvious action to take. But if anyone thinks there's something concrete to do here within the runtime, speak up and/or reopen the issue. Thanks. |
I feel like there might be a doc bug here but I also feel like it's pretty clearly established that there's no general guarantee that a finalizer is ever reached. It seems like a sort of weird special case that tiny objects would be more vulnerable to this, and I'm sure someone could make it behave badly, but... there's pretty decent warnings already. Sorry for the belated response, I apparently missed any earlier notifications, but I'm fine with this being closed as-is. |
@seebs I think a paragraph warning about SetFinalizer's semantics on objects allocated with the tiny allocator seems reasonable. If someone wants to send a CL for that, that would be great. Please send to me, @mknyszek, and @ianlancetaylor. Thanks. |
I just thought of a messy special case for this; imagine for some reason a pair of objects, both small enough to get into tiny allocator blocks, such that one of them controls (but doesn't just contain) a reference to the other, so when the first one is finalized, it eliminates the sole reference to the other. As long as the allocations are separate, the GC will eventually free them, but if they end up in the same tiny allocator block, you actually just get stuck forever; the finalizer won't ever run, because the finalizer is what would have removed the reference to the other thing in that block. I wonder whether this could actually happen, but my intuition would be that "object and the small thing it's referring to get allocated right next to each other" might be common. |
https://play.golang.org/p/1qSh5EY_mzU In which |
|
Change https://golang.org/cl/337391 mentions this issue: |
What version of Go are you using (
go version
)?playground (presumably 1.16ish?), also 1.16.5
Does this issue reproduce with the latest release?
I believe so.
What operating system and processor architecture are you using (
go env
)?playground or amd64 linux
What did you do?
Original reproducer credit goes to BenLubar in libera.chat #go-nuts. Basically: Set finalizers on every object in a small program, then run runtime.GC several times.
https://play.golang.org/p/kdX7i36g8TD
Uncomment the creation of "x", and use it in some way, and suddenly all six finalizers run. I originally thought this was size-related, and depended on x being the same size as those objects, but it doesn't seem that's the case.
What did you expect to see?
All finalizers.
What did you see instead?
All finalizers but the first one.
Allocating anything else before this makes them all show up. I don't have the tooling handy to tell immediately whether the issue is that the first item isn't getting collected, or that the finalizer for it isn't running, but I'm pretty sure the finalizers should run.
Behavior not affected by moving the allocations into another function or other things like that.
The text was updated successfully, but these errors were encountered: