Skip to content

Commit

Permalink
chore: add fast path for ints, fixed ints and floats
Browse files Browse the repository at this point in the history
Since typed slice appender did not produce enough performance benefit, we try the next thing: this commit implements fast path decoding for slices of primitives. Comparison:
BenchmarkSliceOld
BenchmarkSliceOld-10     	 1591179	       764.2 ns/op	     368 B/op	      10 allocs/op
BenchmarkSlice
BenchmarkSlice-10     	 3551311	       337.7 ns/op	     248 B/op	       5 allocs/op

Signed-off-by: Dmitriy Matrenichev <[email protected]>
  • Loading branch information
DmitriyMV committed Aug 4, 2022
1 parent 6427893 commit aa7ee6c
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 4 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-08-03T15:33:56Z by kres latest.
# Generated on 2022-08-04T13:22:07Z by kres latest.

**
!messages
Expand All @@ -19,6 +19,7 @@
!protobuf_test.go
!type_cache.go
!unmarshal.go
!unmarshal_fastpath.go
!go.mod
!go.sum
!.golangci.yml
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-08-03T15:33:56Z by kres latest.
# Generated on 2022-08-04T13:22:07Z by kres latest.

ARG TOOLCHAIN

Expand Down Expand Up @@ -64,6 +64,7 @@ COPY ./predefined_types.go ./predefined_types.go
COPY ./protobuf_test.go ./protobuf_test.go
COPY ./type_cache.go ./type_cache.go
COPY ./unmarshal.go ./unmarshal.go
COPY ./unmarshal_fastpath.go ./unmarshal_fastpath.go
RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null

# runs protobuf compiler
Expand Down
6 changes: 6 additions & 0 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ func BenchmarkCustom(b *testing.B) {
b.Fatal(err)
}
}

require.Equal(b, o.Field.Value+2, target.Field.Value)
}

func BenchmarkSlice(b *testing.B) {
Expand Down Expand Up @@ -106,6 +108,8 @@ func BenchmarkSlice(b *testing.B) {
b.Fatal(err)
}
}

require.Equal(b, o, *target)
}

func BenchmarkString(b *testing.B) {
Expand Down Expand Up @@ -138,4 +142,6 @@ func BenchmarkString(b *testing.B) {
b.Fatal(err)
}
}

require.Equal(b, o, *target)
}
13 changes: 11 additions & 2 deletions unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ func (u *unmarshaller) slice(dst reflect.Value, decodedBytes []byte) error {
if wiretype < 0 { // Other unpacked repeated types
// Just unpack and append one value from decodedBytes.
elem := reflect.New(elemType).Elem()
if err := u.putInto(elem, protowire.BytesType, 0, decodedBytes); err != nil {
if err = u.putInto(elem, protowire.BytesType, 0, decodedBytes); err != nil {
return err
}

Expand All @@ -463,6 +463,15 @@ func (u *unmarshaller) slice(dst reflect.Value, decodedBytes []byte) error {
return nil
}

ok, err = tryDecodePredefinedSlice(wiretype, decodedBytes, dst)
if err != nil {
return err
}

if ok {
return nil
}

sw := sequenceWrapper{
seq: dst,
}
Expand All @@ -489,7 +498,7 @@ func getWiretypeFor(elemType reflect.Type) (protowire.Type, error) {
case reflect.Bool, reflect.Int32, reflect.Int64, reflect.Int,
reflect.Uint32, reflect.Uint64, reflect.Uint:
if (elemType.Kind() == reflect.Int || elemType.Kind() == reflect.Uint) && elemType.Size() < 8 {
return 0, errors.New("detected a 32bit machine, please either use (u)int64 or (u)int32")
return -1, errors.New("detected a 32bit machine, please either use (u)int64 or (u)int32")
}

switch elemType {
Expand Down
142 changes: 142 additions & 0 deletions unmarshal_fastpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package protoenc

import (
"errors"
"math"
"reflect"
"unsafe"

"google.golang.org/protobuf/encoding/protowire"
)

var predefiniedDecoders = map[reflect.Type]func(buf []byte, dst reflect.Value) (bool, error){
typeOf[[]int](): decodeIntSlice[int],
typeOf[[]int32](): decodeIntSlice[int32],
typeOf[[]int64](): decodeIntSlice[int64],
typeOf[[]uint](): decodeIntSlice[uint],
typeOf[[]uint32](): decodeIntSlice[uint32],
typeOf[[]uint64](): decodeIntSlice[uint64],
typeOf[[]FixedU32](): decodeFixed32[FixedU32],
typeOf[[]FixedS32](): decodeFixed32[FixedS32],
typeOf[[]FixedU64](): decodeFixed64[FixedU64],
typeOf[[]FixedS64](): decodeFixed64[FixedS64],
typeOf[[]float32](): decodeFloat32,
typeOf[[]float64](): decodeFloat64,
}

func tryDecodePredefinedSlice(wiretype protowire.Type, buf []byte, dst reflect.Value) (bool, error) {
switch wiretype { //nolint:exhaustive
case protowire.VarintType, protowire.Fixed32Type, protowire.Fixed64Type:
fn, ok := predefiniedDecoders[dst.Type()]
if !ok {
return false, nil
}

return fn(buf, dst)
default:
return false, nil
}
}

type integer interface {
int32 | int64 | uint32 | uint64 | int | uint
}

func decodeIntSlice[I integer](buf []byte, dst reflect.Value) (bool, error) {
var result []I

for len(buf) > 0 {
v, n := protowire.ConsumeVarint(buf)
if n < 0 {
return false, errors.New("bad protobuf varint value")
}

buf = buf[n:]

result = append(result, I(v))
}

*(*[]I)(unsafe.Pointer(dst.UnsafeAddr())) = result

return true, nil
}

func decodeFixed32[F FixedU32 | FixedS32](buf []byte, dst reflect.Value) (bool, error) {
var result []F

for len(buf) > 0 {
v, n := protowire.ConsumeFixed32(buf)
if n < 0 {
return false, errors.New("bad protobuf 32-bit value")
}

buf = buf[n:]

result = append(result, F(v))
}

*(*[]F)(unsafe.Pointer(dst.UnsafeAddr())) = result

return true, nil
}

func decodeFixed64[F FixedU64 | FixedS64](buf []byte, dst reflect.Value) (bool, error) {
var result []F

for len(buf) > 0 {
v, n := protowire.ConsumeFixed64(buf)
if n < 0 {
return false, errors.New("bad protobuf 64-bit value")
}

buf = buf[n:]

result = append(result, F(v))
}

*(*[]F)(unsafe.Pointer(dst.UnsafeAddr())) = result

return true, nil
}

func decodeFloat32(buf []byte, dst reflect.Value) (bool, error) {
var result []float32

for len(buf) > 0 {
v, n := protowire.ConsumeFixed32(buf)
if n < 0 {
return false, errors.New("bad protobuf 32-bit value")
}

buf = buf[n:]

result = append(result, math.Float32frombits(v))
}

*(*[]float32)(unsafe.Pointer(dst.UnsafeAddr())) = result

return true, nil
}

func decodeFloat64(buf []byte, dst reflect.Value) (bool, error) {
var result []float64

for len(buf) > 0 {
v, n := protowire.ConsumeFixed64(buf)
if n < 0 {
return false, errors.New("bad protobuf 64-bit value")
}

buf = buf[n:]

result = append(result, math.Float64frombits(v))
}

*(*[]float64)(unsafe.Pointer(dst.UnsafeAddr())) = result

return true, nil
}

0 comments on commit aa7ee6c

Please sign in to comment.