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: don't allocate for non-escaping conversions to interface{} #8618
Comments
This thread explains how this issue causes 2 allocs on every call to os.(*File).Write. |
Dmitry started this in CL 3503. Note that this requires improved escape analysis. |
CL https://golang.org/cl/35554 mentions this issue. |
For the reference until this is fixed some of us use ad-hoc printf-style mini language to do text formatting in hot codepaths without allocations. For example if in fmt speak you have s := fmt.Sprintf("hello %d %s %x", 1, "world", []byte("data")) the analog would be buf := xfmt.Buffer{}
buf .S("hello ") .D(1) .C(' ') .S("world") .C(' ') .Xb([]byte("data"))
s := buf.Bytes() It is a bit uglier but runs faster and without allocations:
Details: https://lab.nexedi.com/kirr/go123/commit/1aa677c8 |
Note that when the arguments are constants, they no longer allocate on tip, so this is a bit better than it was. |
@josharian thanks for feedback. For the reference the above benchmark was for tip ( |
What are the status about this issue ? Is anyone working on ? |
@bnjjj I'm not aware of anybody actively working on this. |
One data point here. The gio authors are considering an API design choice: whether to use an interface type or a function type. Using the interface type results in nicer code (when there's a literal struct, it can be used directly rather than via a method expression to obtain the function value), but results in allocations, and avoiding allocations is a key consideration in that context. To illustrate with a couple of small examples: This program, using a function type, does not incur any allocations. The required closures can all be allocated on the stack. https://play.golang.org/p/FC4taLlhVGz This equivalent program, using an interface type, incurs an allocation for each interface conversion. https://play.golang.org/p/XPfcNSMPt82 Given that interfaces are often more idiomatic than function types in Go, it would be nice if they could be used interchangeably without a significant performance difference. |
As of five years ago(!), this needed escape analysis improvements. Those stalled; see CL 3503. Maybe @mdempsky could weigh in. |
This was quite a struggle to do in a nice way! 1. Due to golang/go#8618 anything you pass to a fmt.* call gets copied to the heap. Eliminating that was simply replacing fmt.Fprintln with the bytes.Buffer calls and calling os.Stderr.Write directly. 2. Calling time.Duration.String() unfortunately forces a copy to the heap because it returns a string copy of its internal byte slice on the stack. So I had to reimplement that in a way that just writes to my buffer. The challenge was keeping the implementation under 100 lines of code. To help with that I skipped the <1s printing case the standard lib has. Copying the official implementation and patching it would probably result in more efficient code but it is long.
EncodeEscapedChar (which is called in EncodeSQLStringWithFlags) is pretty optimized, but for escaping a multibyte character it was using fmt.FPrintf, which means every multibyte character ended up on the heap due to golang/go#8618. This had a noticeable impact in changefeed benchmarking. This commit just hand-compiles the two formatting strings that were being used into reasonably efficient go, eliminating the allocs. Benchmark encoding the first 10000 runes shows a 4x speedup: Before: BenchmarkEncodeNonASCIISQLString-16 944 1216130 ns/op After: BenchmarkEncodeNonASCIISQLString-16 3468 300777 ns/op Release note: None
EncodeEscapedChar (which is called in EncodeSQLStringWithFlags) is pretty optimized, but for escaping a multibyte character it was using fmt.FPrintf, which means every multibyte character ended up on the heap due to golang/go#8618. This had a noticeable impact in changefeed benchmarking. This commit just hand-compiles the two formatting strings that were being used into reasonably efficient go, eliminating the allocs. Benchmark encoding the first 10000 runes shows a 4x speedup: Before: BenchmarkEncodeNonASCIISQLString-16 944 1216130 ns/op After: BenchmarkEncodeNonASCIISQLString-16 3468 300777 ns/op Release note: None
88425: colexec: use tree.DNull when projection is called on null input r=DrewKimball a=DrewKimball Most projections skip rows for which one or more arguments are null, and just output a null for those rows. However, some projections can actually accept null arguments. Previously, we were using the values from the vec even when the `Nulls` bitmap was set for that row, which invalidates the data in the vec for that row. This could cause a non-null value to be unexpectedly concatenated to an array when an argument was null (nothing should be added to the array in this case). This commit modifies the projection operators that operate on datum-backed vectors to explicitly set the argument to `tree.DNull` in the case when the `Nulls` bitmap is set. This ensures that the projection is not performed with the invalid (and arbitrary) value in the datum vec at that index. Fixes #87919 Release note (bug fix): Fixed a bug in `Concat` projection operators for arrays that could cause non-null values to be added to the array when one of the arguments was null. 88671: util: avoid allocations when escaping multibyte characters r=[miretskiy,yuzefovich] a=HonoreDB EncodeEscapedChar (which is called in EncodeSQLStringWithFlags) is pretty optimized, but for escaping a multibyte character it was using fmt.FPrintf, which means every multibyte character ended up on the heap due to golang/go#8618. This had a noticeable impact in changefeed benchmarking. This commit just hand-compiles the two formatting strings that were being used into reasonably efficient go, eliminating the allocs. Benchmark encoding the first 10000 runes shows a 4x speedup: Before: BenchmarkEncodeNonASCIISQLString-16 944 1216130 ns/op After: BenchmarkEncodeNonASCIISQLString-16 3468 300777 ns/op Release note: None Co-authored-by: DrewKimball <drewk@cockroachlabs.com> Co-authored-by: Aaron Zinger <zinger@cockroachlabs.com>
EncodeEscapedChar (which is called in EncodeSQLStringWithFlags) is pretty optimized, but for escaping a multibyte character it was using fmt.FPrintf, which means every multibyte character ended up on the heap due to golang/go#8618. This had a noticeable impact in changefeed benchmarking. This commit just hand-compiles the two formatting strings that were being used into reasonably efficient go, eliminating the allocs. Benchmark encoding the first 10000 runes shows a 4x speedup: Before: BenchmarkEncodeNonASCIISQLString-16 944 1216130 ns/op After: BenchmarkEncodeNonASCIISQLString-16 3468 300777 ns/op Release note: None
EncodeEscapedChar (which is called in EncodeSQLStringWithFlags) is pretty optimized, but for escaping a multibyte character it was using fmt.FPrintf, which means every multibyte character ended up on the heap due to golang/go#8618. This had a noticeable impact in changefeed benchmarking. This commit just hand-compiles the two formatting strings that were being used into reasonably efficient go, eliminating the allocs. Benchmark encoding the first 10000 runes shows a 4x speedup: Before: BenchmarkEncodeNonASCIISQLString-16 944 1216130 ns/op After: BenchmarkEncodeNonASCIISQLString-16 3468 300777 ns/op Release note: None
FWIW, I was curious what the current gap is for avoiding allocations with the fmt print function arguments. As far as I could see, it looks like as of Go 1.21, there are ~6 reasons val := 1000
fmt.Sprintf("%d", val) I did an exploratory pass on some possible solutions a couple of months ago, which I recently sent as stack of CLs. Some of the CLs are likely wrong, but part of my hope is that sometimes if someone helps sketch the contours of a problem, then frequently other people are better able to jump in with solutions or their own explorations (whether those "other people" are experts, or just other people who are curious). Brief summary of the ~6 reasons:
By the end of my first-cut stack (as of CL 528538), arguments to Sprintf like the Point struct here no longer get heap allocated: type Point struct {x, y int}
p := Point{1, 2}
fmt.Sprintf("%v", p) Some of those CLs are more-or-less shots in the dark, but I tried to put some context in the CL descriptions and elsewhere in the hopes of other gophers jumping in with alternative solutions (or suggestions for corner cases to test, or questions, or ideas for improvement and so on...). The CLs all pass all.bash and pass the older TryBots (but hit some LUCI-specific issues with the new TryBots). If you are new to the escape analysis code:
|
Change https://go.dev/cl/524938 mentions this issue: |
Change https://go.dev/cl/528535 mentions this issue: |
Change https://go.dev/cl/530095 mentions this issue: |
Change https://go.dev/cl/530096 mentions this issue: |
Change https://go.dev/cl/530097 mentions this issue: |
Change https://go.dev/cl/524944 mentions this issue: |
Change https://go.dev/cl/524945 mentions this issue: |
Change https://go.dev/cl/528539 mentions this issue: |
Change https://go.dev/cl/524937 mentions this issue: |
The text was updated successfully, but these errors were encountered: