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
proposal: encoding/binary: cache dataSize across invocations of Read and Write #34471
Comments
Change https://golang.org/cl/199539 mentions this issue: |
The above is my straw man CL, hopefully it can spur some discussion. |
Adding a map to the default encoder will mean that some programs will permanently burn memory that would prefer not to lose forever. I'm not sure that's the best choice. |
@ianlancetaylor @bradfitz can either of you take another look? |
Looking at https://golang.org/cl/199539, it seems that type Reader struct { ... }
func (r *Reader) Read(r io.Reader, order ByteOrder, data interface{}) error
type Writer struct { ... }
func (w *Writer) Write(w io.Writer, order ByteOrder, data interface{}) error Whether the existing It seems odd to have to pass in the type Reader ...
func NewReader(io.Reader, ByteOrder) *Reader
func (r *Reader) Decode(interface{}) error
type Writer ...
func NewWriter(io.Writer, ByteOrder) *Writer
func (w *Writer) Encode(interface{}) error |
Yes, that's true. I figured it would be better to stick with the terminology of
This isn't true for the two use cases I have in mind. First, I'm reading
Second, I use I think those improvements are nice, but as always looked better in the microbenchmarks. I also think that the second use case would benefit from being able to avoid |
You are saying that you vary the If you want to continue with this proposal, do you want to propose a different API? |
Please no. encoding/binary can be made arbitrarily more complex in an effort to make it "fast". It was never intended to be fast, only convenient. |
The size of the map is presumably proportional to the number of distinct types passed to Is it possible to detect types synthesized by the |
That's correct.
Agreed. In fact the package is so convenient that there isn't much else out there, see the godoc.org link I referenced earlier. My thinking is that the ecosystem benefits if we can get a substantial speed up for a very commonly used package.
I think that is correct. I was looking at |
Adding a sync.Map caching just the struct sizes makes a big difference:
diff --git a/src/encoding/binary/binary.go b/src/encoding/binary/binary.go
index 8c2d1d9da4..8b1e97cb2d 100644
--- a/src/encoding/binary/binary.go
+++ b/src/encoding/binary/binary.go
@@ -26,6 +26,7 @@ import (
"io"
"math"
"reflect"
+ "sync"
)
// A ByteOrder specifies how to convert byte sequences into
@@ -377,6 +378,8 @@ func dataSize(v reflect.Value) int {
return sizeof(v.Type())
}
+var structSize sync.Map // reflect.Type -> size
+
// sizeof returns the size >= 0 of variables for the given type or -1 if the type is not acceptable.
func sizeof(t reflect.Type) int {
switch t.Kind() {
@@ -386,6 +389,9 @@ func sizeof(t reflect.Type) int {
}
case reflect.Struct:
+ if size, ok := structSize.Load(t); ok {
+ return size.(int)
+ }
sum := 0
for i, n := 0, t.NumField(); i < n; i++ {
s := sizeof(t.Field(i).Type)
@@ -394,6 +400,9 @@ func sizeof(t reflect.Type) int {
}
sum += s
}
+ if t.Name() != "" { // don't cache reflect-created types
+ structSize.Store(t, sum)
+ }
return sum
case reflect.Bool,
I'm fine with that. It's a bounded number of structs. |
I've updated the CL based on your code @bradfitz, with a little twist: we only cache the result of |
Given that the code is so small and the memory footprint is bounded, I withdraw my objection to adding this cache. Does anyone else still object? Otherwise it seems like this is a likely accept. Leaving open a week for final comments. |
As of current tip (
dfbc9c83a910c79cb3cc34dbfaed3c436e1b6ecb
), usingbinary.Read
to repeatedly unmarshal a struct duplicates a lot of work. This is because the size of the struct is re-computed on every invocation, which causes a lot of work via reflection.You can observe this by looking at a CPU profile of the
ReadStruct
benchmark from the standard library:Here,
binary.dataSize
accounts for almost 60% of runtime, even though the size of a type is fixed and does not change between invocations.I've written a patch which caches the result of
binary.dataSize
in async.Map
, with the following effect on the benchmark:As you can see, we pretty much recoup the 60% seen in the CPU profile. I think
Write
may see a similar speed up, but haven't checked. However, the patch has a problem: the cache can grow without bounds, and there is no way to reset it except by restarting the program.I think the correct solution to this would be to introduce something like an
Encoder
/Decoder
tobinary
which "owns" the cache. But it seems like this might go against the spirit of the package. Looking at https://godoc.org/?q=binary there are many programs that would benefit from a faster Read / Write, so I'd like to explore this option nonetheless.The text was updated successfully, but these errors were encountered: