Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
encoding/binary: use an offset instead of slicing
While running make.bash, over 5% of all pointer writes come from encoding/binary doing struct reads. This change replaces slicing during such reads with an offset. This avoids updating the slice pointer with every struct field read or write. This has no impact when the write barrier is off. Running the benchmarks with GOGC=1, however, shows significant improvement: name old time/op new time/op delta ReadStruct-8 13.2µs ± 6% 10.1µs ± 5% -23.24% (p=0.000 n=10+10) name old speed new speed delta ReadStruct-8 5.69MB/s ± 6% 7.40MB/s ± 5% +30.18% (p=0.000 n=10+10) Change-Id: I22904263196bfeddc38abe8989428e263aee5253 Reviewed-on: https://go-review.googlesource.com/98757 Run-TryBot: Josh Bleecher Snyder <josharian@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org>
- Loading branch information
b854339
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.
@josharian thinking out loud maybe it is better to instead teach the compiler to know that slice updates with destination array pointer pointing to the same array's underlying object as of original slice, e.g. as in
does not need write barrier at all?
The rationale (to my limited write barriers understanding) is that it is whole allocated object that is shaded by write barrier:
(https://github.com/golang/go/blob/d7eb4901/src/runtime/mbarrier.go#L21)
(https://github.com/golang/go/blob/d7eb4901/src/runtime/mgcmark.go#L1211)
(https://github.com/golang/go/blob/d7eb4901/src/runtime/mbitmap.go#L354)
and so for
buf = buf[1:]
the slice, either before or after the update, will be pointing to inside the same allocated object -> thus the mutator for sure won't hide the allocated object from GC and so there is no need to shade it with such updates.Doing so in the compiler will fix the performance issue this patch solves, as well as automatically remove write barriers in other, probably many, places thus helping not only speed but also code size (#6853).
I appologize if there is something trivial preventing doing this, that I missed.
Kirill
/cc @aclements
b854339
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.
The danger here is subtle. We haven't done this because the update to buf could be racy and if we did this that race could break GC. Specifically, if one goroutine executes
buf = buf[1:]
without a write barrier while another executesbuf = x
with a write barrier, it's possible for the final value ofbuf
to continue pointing to the originalbuf
allocation, but for the garbage collector to think it points to thex
allocation and free the originalbuf
allocation.Of course, racy Go programs are undefined, but right now the garbage collector is immune to these sorts of non-type-safety-breaking "benign" races (this is obviously a somewhat fuzzy argument, since races let you break type safety and that lets you break memory safety, and saying anything formal about "benign" races is notoriously impossible so what I'm saying is probably not true anyway, but we try :)
b854339
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.
@aclements It would be interesting to see the effect such an update would have on code size and performance to know how much we're leaving on the table with this safety check on.
b854339
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.
@aclements thanks for explaining. Am I right that with write-barriers enabled for both racy buf updates, the mechanism that prevents GC from deallocating wrong object is that in the current GC written
ptr
is always shaded too (https://github.com/golang/go/blob/go1.10-1-g678dede7bc/src/runtime/mbarrier.go#L162 (go1.10), https://github.com/golang/go/blob/0add9a4dcf/src/runtime/mwbbuf.go#L226 (tip)) ?By the way the thread that performs
buf = x
with write barrier will always doshade(*slot)
. If there isbuf = buf[1:]
racing withbuf = x
, even if the former does not use write barrier, theshade(*slot)
ofbuf = x
thread will shade originalbuf
object in any case (it either seesbuf
orbuf+1
as*slot
but they both actually lead to the same allocated object). If so, sinceshade
actually greys an object and grey objects never become white - only eventually black, we can say that originalbuf
underlying object will stay alive - not deallocated, and so it is safe to dobuf = buf[1:]
without write barrier.But this goes contrary to what you say, and so there is probably some mistake on my side. Could you please explain where the above logic fails?
I'm GC newbie so please forgive me if I miss something well-known.
b854339
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.
@navytux would you mind opening a new issue and migrating this (interesting) discussion there? We tend to try to keep all conversation on CLs and issues. Thanks!
b854339
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.
@josharian, good idea -> #24314.