-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
cmd/compile: improve inline memrun decision in generated alg eq routines #38494
Comments
For small n (<16), the minimum number of comparisons is the number of bits set in n. |
@renthraysk I don't follow. What is |
n is the number of bytes to compare. (Or size returned by memrun)
n / 8 is the number of full wide comparisons (eg x86 CMPQ) With alignment restrictions, means have to compare between [0, 7] bytes, w, first to gain alignment.
Or am I missing something? (My familiarity with the go compiler internals is low) |
You can do a bit better than that on unaligned arches using overlapping comparisons. E.g. you can do a 15 byte comparison by comparing bytes 0–7 and then bytes 8–15, instead of 0–7, 8–11, 12–14, 14–15. And even on unaligned arches, you need to treat pointers as having alignment requirements. |
Change https://golang.org/cl/230203 mentions this issue: |
Related (possibly a prerequisite?): #38674. |
type T struct { f float64 a [64]uint64 g float64 } Prior to this change, the generated equality algorithm for T was: func eqT(p, q *T) bool { return p.f == q.f && runtime.memequal(p.a, q.a, 512) && p.g == q.g } In handwritten code, we would normally put the cheapest checks first. This change takes a step in that direction. We now generate: func eqT(p, q *T) bool { return p.f == q.f && p.g == q.g && runtime.memequal(p.a, q.a, 512) } For most types, this also generates considerably shorter code. Examples: runtime .eq."".mstats 406 -> 391 (-3.69%) .eq.""._func 114 -> 101 (-11.40%) .eq."".itab 115 -> 102 (-11.30%) .eq."".scase 125 -> 116 (-7.20%) .eq."".traceStack 119 -> 102 (-14.29%) .eq."".gcControllerState 169 -> 161 (-4.73%) .eq."".sweepdata 121 -> 112 (-7.44%) However, for types in which we make unwise choices about inlining memory-only comparisons (#38494), this generates longer code. Example: cmd/internal/obj .eq."".objWriter 211 -> 214 (+1.42%) .eq."".Addr 185 -> 187 (+1.08%) Fortunately, such cases are not common. Change-Id: I47a27da93c1f88ec71fa350c192f36b29548a217 Reviewed-on: https://go-review.googlesource.com/c/go/+/230203 Run-TryBot: Josh Bleecher Snyder <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
type T struct { f float64 a [64]uint64 g float64 } Prior to this change, the generated equality algorithm for T was: func eqT(p, q *T) bool { return p.f == q.f && runtime.memequal(p.a, q.a, 512) && p.g == q.g } In handwritten code, we would normally put the cheapest checks first. This change takes a step in that direction. We now generate: func eqT(p, q *T) bool { return p.f == q.f && p.g == q.g && runtime.memequal(p.a, q.a, 512) } For most types, this also generates considerably shorter code. Examples: runtime .eq."".mstats 406 -> 391 (-3.69%) .eq.""._func 114 -> 101 (-11.40%) .eq."".itab 115 -> 102 (-11.30%) .eq."".scase 125 -> 116 (-7.20%) .eq."".traceStack 119 -> 102 (-14.29%) .eq."".gcControllerState 169 -> 161 (-4.73%) .eq."".sweepdata 121 -> 112 (-7.44%) However, for types in which we make unwise choices about inlining memory-only comparisons (golang#38494), this generates longer code. Example: cmd/internal/obj .eq."".objWriter 211 -> 214 (+1.42%) .eq."".Addr 185 -> 187 (+1.08%) Fortunately, such cases are not common. Change-Id: I47a27da93c1f88ec71fa350c192f36b29548a217 Reviewed-on: https://go-review.googlesource.com/c/go/+/230203 Run-TryBot: Josh Bleecher Snyder <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
Change https://go.dev/cl/411674 mentions this issue: |
We've noticed that in go1.19 there can be a sizable performance regression due to the heuristics introduced in 3c29aca (more details here). These regressions are rectified by 6605686, which resolved this issue. I didn't see 6605686 on |
We don't generally backport performance fixes. See https://github.com/golang/go/wiki/MinorReleases |
@nicktrav apologies for the temporary performance regression as the optimization work got split across multiple releases. Once 1.19.2 is released it should contain that latest commit which rounds out the work. EDIT: Sorry I was clearly mistaken, no backport means it won't hit the 1.19 release at all and will wait for 1.20. I was hoping to get both CLs in for the 1.19 release but didn't make the freeze. |
cmd/compile/internal/gc/alg.go func geneq contains this code:
The idea here is to inline small, simple memory comparisons instead of calling runtime.memequal.
However, using the number of fields isn't a great heuristic. If one of those fields is (say)
[64]uint64
, inlining it turns into a runtime call anyway. If one of those fields is (say)struct { x, y, z, w uint64 }
, we end up inlining four comparisons instead of the intended one. The heuristic fails in the other direction too: If there are eight fields, but each has typebyte
, we'd be better off doing a single uint64 comparison than calling runtime.memequal.A better plan would be to calculate how much work we actually have to do and then do it optimally, either with the minimal number of inlined comparisons or with a single runtime call.
Roughly, if we need to compare n bytes, figure out how many comparisons are required to accomplish that, and then use that as a threshold. (We already do something like that in walk.go's walkCompareString when comparing a non-constant string to a constant string.) One complication is alignment. On architectures with alignment requirements, comparing 8 bytes could require (say) a 2 byte comparison + a 4 byte comparison + a 2 byte comparison. The other thing that requires care is pointers: Given
struct { f float32; p *int; u uint32 }
, we don't want to pull half ofp
into one register and the other half ofp
into another register.Ideally, we would also use this work calculation in our decision about whether to inline equality comparisons in the first place or call a generated routine (walk.go func walkcompare); we currently count fields there, too.
I believe that this would be challenging but possible for someone with relatively little compiler experience, so marking as help wanted (and spelling things out in more detail than usual).
The text was updated successfully, but these errors were encountered: