Skip to content

proposal: encoding/json: new MarshalAppender interface to make custom marshallers more efficient #34701

@philpearl

Description

@philpearl

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.

  1. the json.Marshaler interface forces you to allocate a []byte to return the result rather than following an 'append' or 'io.Writer' pattern
  2. 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
  3. 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!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions