-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
What version of Go are you using (go version
)?
$ go version go version go1.13 darwin/amd64
Does this issue reproduce with the latest release?
Yes. And with the head of the master branch.
What operating system and processor architecture are you using (go env
)?
go env
Output
$ go env GO111MODULE="off" GOARCH="amd64" GOBIN="" GOCACHE="/Users/phil/Library/Caches/go-build" GOENV="/Users/phil/Library/Application Support/go/env" GOEXE="" GOFLAGS="" GOHOSTARCH="amd64" GOHOSTOS="darwin" GONOPROXY="" GONOSUMDB="" GOOS="darwin" GOPATH="/Users/phil/go" GOPRIVATE="" GOPROXY="https://proxy.golang.org,direct" GOROOT="/usr/local/Cellar/go/1.13/libexec" GOSUMDB="sum.golang.org" GOTMPDIR="" GOTOOLDIR="/usr/local/Cellar/go/1.13/libexec/pkg/tool/darwin_amd64" GCCGO="gccgo" AR="ar" CC="clang" CXX="clang++" CGO_ENABLED="1" GOMOD="" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config" GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/0j/nhsrh_152tg44s03bvq7mrh80000gn/T/go-build757131500=/tmp/go-build -gno-record-gcc-switches -fno-common"
Problem
If you marshal a struct with time.Time field in it there are additional allocations per time field. There appear to be 3 reasons for this.
- the json.Marshaler interface forces you to allocate a []byte to return the result rather than following an 'append' or 'io.Writer' pattern
- the json.Marshaler interface isn't trusted to return good JSON, so there's additional overhead validating and compacting the returned data when copying it into the underlying buffer
- If the MarshalJSON function has a non-pointer receiver there's an allocation creating the interface value to call the function
These inefficiencies exist for any type that implements MarshalJSON, but time.Time is a big issue in code I've been involved with.
Proposal
I think we can remove the first 2 of these by creating a new interface that follows the append pattern and trusts implementations to create compact valid JSON.
// MarshalAppender is implemented by types that can marshal themselves into compact and
// valid JSON. Implementations should append their JSON to the data parameter and return
// the resulting byte slice. MarshalAppender can be implemented more efficiently than
// Marshaler. If a type implements both Marshaler and MarshalAppender then
// MarshalAppender is used in preference.
type MarshalAppender interface {
MarshalAppendJSON(data []byte) ([]byte, error)
}
I think an "Append" model is preferable to using io.Writer as the standard library has many append methods, e.g. time.Time has AppendFormat, and there are many Append functions in strconv. I suspect that means an interface following the append pattern will be less surprising and easier to use efficiently.
I've knocked together a prototype implementation with the following results on a simple benchmark (interestingly I can't find a benchmark that covers using a custom marshaler in the existing codebase).
benchstat before.txt after.txt
name old time/op new time/op delta
MarshalMarshaler-8 128ns ± 0% 94ns ± 8% -26.25% (p=0.000 n=7+8)
MarshalMarshalerEncoder-8 132ns ± 3% 90ns ± 1% -32.12% (p=0.001 n=7+6)
name old alloc/op new alloc/op delta
MarshalMarshaler-8 96.0B ± 0% 24.0B ± 0% -75.00% (p=0.000 n=8+8)
MarshalMarshalerEncoder-8 80.0B ± 0% 8.0B ± 0% -90.00% (p=0.000 n=8+8)
name old allocs/op new allocs/op delta
MarshalMarshaler-8 4.00 ± 0% 2.00 ± 0% -50.00% (p=0.000 n=8+8)
MarshalMarshalerEncoder-8 3.00 ± 0% 1.00 ± 0% -66.67% (p=0.000 n=8+8)
I've be very pleased to attempt to contribute this if the proposal is accepted.
Apologies if I've not followed the proposal process properly - I don't know what I'm doing!