Skip to content
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

Reduce bytes to strings allocs in decoder #367

Merged
merged 4 commits into from
May 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [1.16, 1.17, 1.18, 1.19]
go-version: ['1.16', '1.17', '1.18', '1.19', '1.20']

steps:

Expand Down
79 changes: 74 additions & 5 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/binary"
"io"
"reflect"
"unsafe"
)

type decoder struct {
Expand All @@ -13,9 +14,10 @@ type decoder struct {
fds []int

// The following fields are used to reduce memory allocs.
buf []byte
d float64
y [1]byte
conv *stringConverter
buf []byte
d float64
y [1]byte
}

// newDecoder returns a new decoder that reads values from in. The input is
Expand All @@ -25,6 +27,7 @@ func newDecoder(in io.Reader, order binary.ByteOrder, fds []int) *decoder {
dec.in = in
dec.order = order
dec.fds = fds
dec.conv = newStringConverter(stringConverterBufferSize)
return dec
}

Expand All @@ -34,6 +37,10 @@ func (dec *decoder) Reset(in io.Reader, order binary.ByteOrder, fds []int) {
dec.order = order
dec.pos = 0
dec.fds = fds

if dec.conv == nil {
dec.conv = newStringConverter(stringConverterBufferSize)
}
}

// align aligns the input to the given boundary and panics on error.
Expand Down Expand Up @@ -148,7 +155,7 @@ func (dec *decoder) decode(s string, depth int) interface{} {
p := int(length) + 1
dec.read2buf(p)
dec.pos += p
return string(dec.buf[:len(dec.buf)-1])
return dec.conv.String(dec.buf[:len(dec.buf)-1])
case 'o':
return ObjectPath(dec.decode("s", depth).(string))
case 'g':
Expand All @@ -157,7 +164,7 @@ func (dec *decoder) decode(s string, depth int) interface{} {
dec.read2buf(p)
dec.pos += p
sig, err := ParseSignature(
string(dec.buf[:len(dec.buf)-1]),
dec.conv.String(dec.buf[:len(dec.buf)-1]),
)
if err != nil {
panic(err)
Expand Down Expand Up @@ -310,3 +317,65 @@ type FormatError string
func (e FormatError) Error() string {
return "dbus: wire format error: " + string(e)
}

// stringConverterBufferSize defines the recommended buffer size of 4KB.
// It showed good results in a benchmark when decoding 35KB message,
// see https://github.com/marselester/systemd#testing.
const stringConverterBufferSize = 4096

func newStringConverter(capacity int) *stringConverter {
return &stringConverter{
buf: make([]byte, 0, capacity),
offset: 0,
}
}

// stringConverter converts bytes to strings with less allocs.
// The idea is to accumulate bytes in a buffer with specified capacity
// and create strings with unsafe package using bytes from a buffer.
// For example, 10 "fizz" strings written to a 40-byte buffer
// will result in 1 alloc instead of 10.
//
// Once a buffer is filled, a new one is created with the same capacity.
// Old buffers will be eventually GC-ed
// with no side effects to the returned strings.
type stringConverter struct {
// buf is a temporary buffer where decoded strings are batched.
buf []byte
// offset is a buffer position where the last string was written.
offset int
}

// String converts bytes to a string.
func (c *stringConverter) String(b []byte) string {
n := len(b)
if n == 0 {
return ""
}
// Must allocate because a string doesn't fit into the buffer.
if n > cap(c.buf) {
return string(b)
}

if len(c.buf)+n > cap(c.buf) {
c.buf = make([]byte, 0, cap(c.buf))
c.offset = 0
}
c.buf = append(c.buf, b...)

b = c.buf[c.offset:]
s := toString(b)
c.offset += n
return s
}

// toString converts a byte slice to a string without allocating.
// Starting from Go 1.20 you should use unsafe.String.
func toString(b []byte) string {
var s string
h := (*reflect.StringHeader)(unsafe.Pointer(&s))
h.Data = uintptr(unsafe.Pointer(&b[0]))
h.Len = len(b)

return s
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/godbus/dbus/v5

go 1.12

require golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 // indirect
require golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2