Skip to content

improve allocations in map serialization#105

Merged
whyrusleeping merged 4 commits intomasterfrom
feat/map-ser-allocs
Oct 30, 2024
Merged

improve allocations in map serialization#105
whyrusleeping merged 4 commits intomasterfrom
feat/map-ser-allocs

Conversation

@whyrusleeping
Copy link
Owner

reuse buffers more and use optimized read paths when available.

Before:

why@sirius ~/c/cbor-gen (master)> go test ./testing -run XXX -bench=.
goos: linux
goarch: amd64
pkg: github.com/whyrusleeping/cbor-gen/testing
cpu: AMD Ryzen Threadripper 3970X 32-Core Processor
BenchmarkMarshaling-64         	 1856397	       712.1 ns/op	      48 B/op	       1 allocs/op
BenchmarkUnmarshaling-64       	  199536	      6274 ns/op	    1777 B/op	      21 allocs/op
BenchmarkLinkScan-64           	  729843	      1645 ns/op	     112 B/op	       1 allocs/op
BenchmarkDeferred-64           	  505626	      2223 ns/op	      40 B/op	       2 allocs/op
BenchmarkMapMarshaling-64      	 2326390	       497.8 ns/op	      50 B/op	       3 allocs/op
BenchmarkMapUnmarshaling-64    	  140995	      8113 ns/op	    2043 B/op	      36 allocs/op
PASS
ok  	github.com/whyrusleeping/cbor-gen/testing	10.424s

After:

why@sirius ~/c/cbor-gen (master)> go test ./testing -run XXX -bench=.
goos: linux
goarch: amd64
pkg: github.com/whyrusleeping/cbor-gen/testing
cpu: AMD Ryzen Threadripper 3970X 32-Core Processor
BenchmarkMarshaling-64         	 1755950	       722.5 ns/op	      48 B/op	       1 allocs/op
BenchmarkUnmarshaling-64       	  192193	      6699 ns/op	    1777 B/op	      21 allocs/op
BenchmarkLinkScan-64           	  672280	      1669 ns/op	     112 B/op	       1 allocs/op
BenchmarkDeferred-64           	  656348	      2182 ns/op	      40 B/op	       2 allocs/op
BenchmarkMapMarshaling-64      	 2236473	       536.9 ns/op	      50 B/op	       3 allocs/op
BenchmarkMapUnmarshaling-64    	  183109	      6313 ns/op	    1930 B/op	      20 allocs/op
PASS
ok  	github.com/whyrusleeping/cbor-gen/testing	8.893s

// ReadFullStringIntoBuf will read a string off the given stream, consuming the
// entire cbor item if the string on the stream is longer than the buffer,
// the string is discarded and 'false' is returned
func ReadFullStringIntoBuf(cr *CborReader, buf []byte, maxLength uint64) (int, bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: generally I prefer the Hash version of this:

  1. Take a zero-length buffer with some capacity.
  2. Return a buffer containing the output, appending to the input buffer if possible.

That way it's possible to just pass nil and let the function do the allocation for you. You also won't need to handle slicing, etc.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the main reason i added this was to avoid any allocations, i can make it append if necessary but it feels more prone to being misused in that case

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. You want to be able to skip fields that are too long entirely. I guess that makes sense, it just seems like a bit of a sharp edge.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah... i would make it private but then generated files can't call it

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill add an extra comment

peeker.go Outdated
return b, nil
}

func (p *peeker) ReadByteBuf(buf []byte) (byte, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If peeker.ReadByte is allocating buf, maybe just replace peeker.lastByte with a [1]byte and use that directly in io.ReadFull?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, thats a good idea

gen.go Outdated

if !ok {
// Field doesn't exist on this type, so ignore it
cbg.ScanForLinks(cr, func(cid.Cid){})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not have a better way to ignore fields? Honestly, I'd special-case Deferred into an Ignored type that just drops everything.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we need to handle errors.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah thats the way we've been doing it so far, probably makes sense to have a more dedicated thing for it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, this is probably fine (assuming you check for errors). It ensures that any CIDs contained in such fields are valid.

io.go Outdated
Comment on lines 71 to 72
if len(scratchBuf) < int(extra) {
return cid.Undef, fmt.Errorf("scratchBuf not large enough for cid")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we usually allocate header-sized scratch bufs?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this is actually a different path entirely (not used in codebase yet). I wanted this for doing some manual/optimized reading of certain objects

io.go Outdated
cr.r = GetPeeker(r)
}

func (cr *CborReader) ReadCid(scratchBuf []byte) (cid.Cid, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you planning on using this somewhere?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outside of package, yeah. Its really a separate thing from the rest of this PR, happy to remove for now and bring back in later

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just kind of awkward given that:

  1. Everything else uses the internal scratch buffer.
  2. It will fail if the scratch buffer isn't large enough.

If we need a CID scratch buffer, I'd consider just increasing the size of the internal scratch buffer to fit CIDs.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright, ill just remove this for now. can bring it back later

gen.go Outdated

if !ok {
// Field doesn't exist on this type, so ignore it
cbg.ScanForLinks(cr, func(cid.Cid){})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, this is probably fine (assuming you check for errors). It ensures that any CIDs contained in such fields are valid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants