stackitem: use byte-slice directly during encoding

```
name            old time/op    new time/op    delta
EncodeBinary-8    10.6ms ± 1%     8.4ms ± 1%  -20.94%  (p=0.000 n=9+10)

name            old alloc/op   new alloc/op   delta
EncodeBinary-8    1.64MB ± 0%    2.24MB ± 0%  +36.18%  (p=0.000 n=8+9)

name            old allocs/op  new allocs/op  delta
EncodeBinary-8      131k ± 0%       66k ± 0%  -49.98%  (p=0.000 n=10+10)
```

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgeniy Stratonikov 2021-07-09 15:26:56 +03:00
parent c4eb7db8a5
commit 17a3f17c74
2 changed files with 85 additions and 62 deletions

View file

@ -11,6 +11,7 @@ import (
// from a struct with many fields. // from a struct with many fields.
type BinWriter struct { type BinWriter struct {
w io.Writer w io.Writer
uv []byte
u64 []byte u64 []byte
u32 []byte u32 []byte
u16 []byte u16 []byte
@ -20,11 +21,12 @@ type BinWriter struct {
// NewBinWriterFromIO makes a BinWriter from io.Writer. // NewBinWriterFromIO makes a BinWriter from io.Writer.
func NewBinWriterFromIO(iow io.Writer) *BinWriter { func NewBinWriterFromIO(iow io.Writer) *BinWriter {
u64 := make([]byte, 8) uv := make([]byte, 9)
u64 := uv[:8]
u32 := u64[:4] u32 := u64[:4]
u16 := u64[:2] u16 := u64[:2]
u8 := u64[:1] u8 := u64[:1]
return &BinWriter{w: iow, u64: u64, u32: u32, u16: u16, u8: u8} return &BinWriter{w: iow, uv: uv, u64: u64, u32: u32, u16: u16, u8: u8}
} }
// WriteU64LE writes an uint64 value into the underlying io.Writer in // WriteU64LE writes an uint64 value into the underlying io.Writer in
@ -106,23 +108,31 @@ func (w *BinWriter) WriteVarUint(val uint64) {
return return
} }
n := PutVarUint(w.uv, val)
w.WriteBytes(w.uv[:n])
}
// PutVarUint puts val in varint form to the pre-allocated buffer.
func PutVarUint(data []byte, val uint64) int {
_ = data[8]
if val < 0xfd { if val < 0xfd {
w.WriteB(byte(val)) data[0] = byte(val)
return return 1
} }
if val < 0xFFFF { if val < 0xFFFF {
w.WriteB(byte(0xfd)) data[0] = byte(0xfd)
w.WriteU16LE(uint16(val)) binary.LittleEndian.PutUint16(data[1:], uint16(val))
return return 3
} }
if val < 0xFFFFFFFF { if val < 0xFFFFFFFF {
w.WriteB(byte(0xfe)) data[0] = byte(0xfe)
w.WriteU32LE(uint32(val)) binary.LittleEndian.PutUint32(data[1:], uint32(val))
return return 5
} }
w.WriteB(byte(0xff)) data[0] = byte(0xff)
w.WriteU64LE(val) binary.LittleEndian.PutUint64(data[1:], val)
return 9
} }
// WriteBytes writes a variable byte into the underlying io.Writer without prefix. // WriteBytes writes a variable byte into the underlying io.Writer without prefix.

View file

@ -19,38 +19,35 @@ var ErrUnserializable = errors.New("unserializable")
// serContext is an internal serialization context. // serContext is an internal serialization context.
type serContext struct { type serContext struct {
*io.BinWriter uv [9]byte
buf *io.BufBinWriter data []byte
allowInvalid bool allowInvalid bool
seen map[Item]bool seen map[Item]bool
} }
// Serialize encodes given Item into the byte slice. // Serialize encodes given Item into the byte slice.
func Serialize(item Item) ([]byte, error) { func Serialize(item Item) ([]byte, error) {
w := io.NewBufBinWriter()
sc := serContext{ sc := serContext{
BinWriter: w.BinWriter,
buf: w,
allowInvalid: false, allowInvalid: false,
seen: make(map[Item]bool), seen: make(map[Item]bool),
} }
sc.serialize(item) err := sc.serialize(item)
if w.Err != nil { if err != nil {
return nil, w.Err return nil, err
} }
return w.Bytes(), nil return sc.data, nil
} }
// EncodeBinary encodes given Item into the given BinWriter. It's // EncodeBinary encodes given Item into the given BinWriter. It's
// similar to io.Serializable's EncodeBinary, but works with Item // similar to io.Serializable's EncodeBinary, but works with Item
// interface. // interface.
func EncodeBinary(item Item, w *io.BinWriter) { func EncodeBinary(item Item, w *io.BinWriter) {
sc := serContext{ data, err := Serialize(item)
BinWriter: w, if err != nil {
allowInvalid: false, w.Err = err
seen: make(map[Item]bool), return
} }
sc.serialize(item) w.WriteBytes(data)
} }
// EncodeBinaryProtected encodes given Item into the given BinWriter. It's // EncodeBinaryProtected encodes given Item into the given BinWriter. It's
@ -59,88 +56,104 @@ func EncodeBinary(item Item, w *io.BinWriter) {
// (like recursive array) is encountered it just writes special InvalidT // (like recursive array) is encountered it just writes special InvalidT
// type of element to w. // type of element to w.
func EncodeBinaryProtected(item Item, w *io.BinWriter) { func EncodeBinaryProtected(item Item, w *io.BinWriter) {
bw := io.NewBufBinWriter()
sc := serContext{ sc := serContext{
BinWriter: bw.BinWriter,
buf: bw,
allowInvalid: true, allowInvalid: true,
seen: make(map[Item]bool), seen: make(map[Item]bool),
} }
sc.serialize(item) err := sc.serialize(item)
if bw.Err != nil { if err != nil {
w.WriteBytes([]byte{byte(InvalidT)}) w.WriteBytes([]byte{byte(InvalidT)})
return return
} }
w.WriteBytes(bw.Bytes()) w.WriteBytes(sc.data)
} }
func (w *serContext) serialize(item Item) { func (w *serContext) serialize(item Item) error {
if w.Err != nil {
return
}
if w.seen[item] { if w.seen[item] {
w.Err = ErrRecursive return ErrRecursive
return
} }
switch t := item.(type) { switch t := item.(type) {
case *ByteArray: case *ByteArray:
w.WriteBytes([]byte{byte(ByteArrayT)}) w.data = append(w.data, byte(ByteArrayT))
w.WriteVarBytes(t.Value().([]byte)) data := t.Value().([]byte)
w.appendVarUint(uint64(len(data)))
w.data = append(w.data, data...)
case *Buffer: case *Buffer:
w.WriteBytes([]byte{byte(BufferT)}) w.data = append(w.data, byte(BufferT))
w.WriteVarBytes(t.Value().([]byte)) data := t.Value().([]byte)
w.appendVarUint(uint64(len(data)))
w.data = append(w.data, data...)
case *Bool: case *Bool:
w.WriteBytes([]byte{byte(BooleanT)}) w.data = append(w.data, byte(BooleanT))
w.WriteBool(t.Value().(bool)) if t.Value().(bool) {
w.data = append(w.data, 1)
} else {
w.data = append(w.data, 0)
}
case *BigInteger: case *BigInteger:
w.WriteBytes([]byte{byte(IntegerT)}) w.data = append(w.data, byte(IntegerT))
w.WriteVarBytes(bigint.ToBytes(t.Value().(*big.Int))) data := bigint.ToBytes(t.Value().(*big.Int))
w.appendVarUint(uint64(len(data)))
w.data = append(w.data, data...)
case *Interop: case *Interop:
if w.allowInvalid { if w.allowInvalid {
w.WriteBytes([]byte{byte(InteropT)}) w.data = append(w.data, byte(InteropT))
} else { } else {
w.Err = fmt.Errorf("%w: Interop", ErrUnserializable) return fmt.Errorf("%w: Interop", ErrUnserializable)
} }
case *Array, *Struct: case *Array, *Struct:
w.seen[item] = true w.seen[item] = true
_, isArray := t.(*Array) _, isArray := t.(*Array)
if isArray { if isArray {
w.WriteBytes([]byte{byte(ArrayT)}) w.data = append(w.data, byte(ArrayT))
} else { } else {
w.WriteBytes([]byte{byte(StructT)}) w.data = append(w.data, byte(StructT))
} }
arr := t.Value().([]Item) arr := t.Value().([]Item)
w.WriteVarUint(uint64(len(arr))) w.appendVarUint(uint64(len(arr)))
for i := range arr { for i := range arr {
w.serialize(arr[i]) if err := w.serialize(arr[i]); err != nil {
return err
}
} }
delete(w.seen, item) delete(w.seen, item)
case *Map: case *Map:
w.seen[item] = true w.seen[item] = true
w.WriteBytes([]byte{byte(MapT)}) elems := t.Value().([]MapElement)
w.WriteVarUint(uint64(len(t.Value().([]MapElement)))) w.data = append(w.data, byte(MapT))
for i := range t.Value().([]MapElement) { w.appendVarUint(uint64(len(elems)))
w.serialize(t.Value().([]MapElement)[i].Key) for i := range elems {
w.serialize(t.Value().([]MapElement)[i].Value) if err := w.serialize(elems[i].Key); err != nil {
return err
}
if err := w.serialize(elems[i].Value); err != nil {
return err
}
} }
delete(w.seen, item) delete(w.seen, item)
case Null: case Null:
w.WriteB(byte(AnyT)) w.data = append(w.data, byte(AnyT))
case nil: case nil:
if w.allowInvalid { if w.allowInvalid {
w.WriteBytes([]byte{byte(InvalidT)}) w.data = append(w.data, byte(InvalidT))
} else { } else {
w.Err = fmt.Errorf("%w: nil", ErrUnserializable) return fmt.Errorf("%w: nil", ErrUnserializable)
} }
} }
if w.Err == nil && w.buf != nil && w.buf.Len() > MaxSize { if len(w.data) > MaxSize {
w.Err = errTooBigSize return errTooBigSize
} }
return nil
}
func (w *serContext) appendVarUint(val uint64) {
n := io.PutVarUint(w.uv[:], val)
w.data = append(w.data, w.uv[:n]...)
} }
// Deserialize decodes Item from the given byte slice. // Deserialize decodes Item from the given byte slice.