Skip to content

Commit aa7ee6c

Browse files
committed
chore: add fast path for ints, fixed ints and floats
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 <dmitry.matrenichev@siderolabs.com>
1 parent 6427893 commit aa7ee6c

File tree

5 files changed

+163
-4
lines changed

5 files changed

+163
-4
lines changed

.dockerignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
22
#
3-
# Generated on 2022-08-03T15:33:56Z by kres latest.
3+
# Generated on 2022-08-04T13:22:07Z by kres latest.
44

55
**
66
!messages
@@ -19,6 +19,7 @@
1919
!protobuf_test.go
2020
!type_cache.go
2121
!unmarshal.go
22+
!unmarshal_fastpath.go
2223
!go.mod
2324
!go.sum
2425
!.golangci.yml

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

77
ARG TOOLCHAIN
88

@@ -64,6 +64,7 @@ COPY ./predefined_types.go ./predefined_types.go
6464
COPY ./protobuf_test.go ./protobuf_test.go
6565
COPY ./type_cache.go ./type_cache.go
6666
COPY ./unmarshal.go ./unmarshal.go
67+
COPY ./unmarshal_fastpath.go ./unmarshal_fastpath.go
6768
RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null
6869

6970
# runs protobuf compiler

benchmarks_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ func BenchmarkCustom(b *testing.B) {
7474
b.Fatal(err)
7575
}
7676
}
77+
78+
require.Equal(b, o.Field.Value+2, target.Field.Value)
7779
}
7880

7981
func BenchmarkSlice(b *testing.B) {
@@ -106,6 +108,8 @@ func BenchmarkSlice(b *testing.B) {
106108
b.Fatal(err)
107109
}
108110
}
111+
112+
require.Equal(b, o, *target)
109113
}
110114

111115
func BenchmarkString(b *testing.B) {
@@ -138,4 +142,6 @@ func BenchmarkString(b *testing.B) {
138142
b.Fatal(err)
139143
}
140144
}
145+
146+
require.Equal(b, o, *target)
141147
}

unmarshal.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ func (u *unmarshaller) slice(dst reflect.Value, decodedBytes []byte) error {
454454
if wiretype < 0 { // Other unpacked repeated types
455455
// Just unpack and append one value from decodedBytes.
456456
elem := reflect.New(elemType).Elem()
457-
if err := u.putInto(elem, protowire.BytesType, 0, decodedBytes); err != nil {
457+
if err = u.putInto(elem, protowire.BytesType, 0, decodedBytes); err != nil {
458458
return err
459459
}
460460

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

466+
ok, err = tryDecodePredefinedSlice(wiretype, decodedBytes, dst)
467+
if err != nil {
468+
return err
469+
}
470+
471+
if ok {
472+
return nil
473+
}
474+
466475
sw := sequenceWrapper{
467476
seq: dst,
468477
}
@@ -489,7 +498,7 @@ func getWiretypeFor(elemType reflect.Type) (protowire.Type, error) {
489498
case reflect.Bool, reflect.Int32, reflect.Int64, reflect.Int,
490499
reflect.Uint32, reflect.Uint64, reflect.Uint:
491500
if (elemType.Kind() == reflect.Int || elemType.Kind() == reflect.Uint) && elemType.Size() < 8 {
492-
return 0, errors.New("detected a 32bit machine, please either use (u)int64 or (u)int32")
501+
return -1, errors.New("detected a 32bit machine, please either use (u)int64 or (u)int32")
493502
}
494503

495504
switch elemType {

unmarshal_fastpath.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package protoenc
6+
7+
import (
8+
"errors"
9+
"math"
10+
"reflect"
11+
"unsafe"
12+
13+
"google.golang.org/protobuf/encoding/protowire"
14+
)
15+
16+
var predefiniedDecoders = map[reflect.Type]func(buf []byte, dst reflect.Value) (bool, error){
17+
typeOf[[]int](): decodeIntSlice[int],
18+
typeOf[[]int32](): decodeIntSlice[int32],
19+
typeOf[[]int64](): decodeIntSlice[int64],
20+
typeOf[[]uint](): decodeIntSlice[uint],
21+
typeOf[[]uint32](): decodeIntSlice[uint32],
22+
typeOf[[]uint64](): decodeIntSlice[uint64],
23+
typeOf[[]FixedU32](): decodeFixed32[FixedU32],
24+
typeOf[[]FixedS32](): decodeFixed32[FixedS32],
25+
typeOf[[]FixedU64](): decodeFixed64[FixedU64],
26+
typeOf[[]FixedS64](): decodeFixed64[FixedS64],
27+
typeOf[[]float32](): decodeFloat32,
28+
typeOf[[]float64](): decodeFloat64,
29+
}
30+
31+
func tryDecodePredefinedSlice(wiretype protowire.Type, buf []byte, dst reflect.Value) (bool, error) {
32+
switch wiretype { //nolint:exhaustive
33+
case protowire.VarintType, protowire.Fixed32Type, protowire.Fixed64Type:
34+
fn, ok := predefiniedDecoders[dst.Type()]
35+
if !ok {
36+
return false, nil
37+
}
38+
39+
return fn(buf, dst)
40+
default:
41+
return false, nil
42+
}
43+
}
44+
45+
type integer interface {
46+
int32 | int64 | uint32 | uint64 | int | uint
47+
}
48+
49+
func decodeIntSlice[I integer](buf []byte, dst reflect.Value) (bool, error) {
50+
var result []I
51+
52+
for len(buf) > 0 {
53+
v, n := protowire.ConsumeVarint(buf)
54+
if n < 0 {
55+
return false, errors.New("bad protobuf varint value")
56+
}
57+
58+
buf = buf[n:]
59+
60+
result = append(result, I(v))
61+
}
62+
63+
*(*[]I)(unsafe.Pointer(dst.UnsafeAddr())) = result
64+
65+
return true, nil
66+
}
67+
68+
func decodeFixed32[F FixedU32 | FixedS32](buf []byte, dst reflect.Value) (bool, error) {
69+
var result []F
70+
71+
for len(buf) > 0 {
72+
v, n := protowire.ConsumeFixed32(buf)
73+
if n < 0 {
74+
return false, errors.New("bad protobuf 32-bit value")
75+
}
76+
77+
buf = buf[n:]
78+
79+
result = append(result, F(v))
80+
}
81+
82+
*(*[]F)(unsafe.Pointer(dst.UnsafeAddr())) = result
83+
84+
return true, nil
85+
}
86+
87+
func decodeFixed64[F FixedU64 | FixedS64](buf []byte, dst reflect.Value) (bool, error) {
88+
var result []F
89+
90+
for len(buf) > 0 {
91+
v, n := protowire.ConsumeFixed64(buf)
92+
if n < 0 {
93+
return false, errors.New("bad protobuf 64-bit value")
94+
}
95+
96+
buf = buf[n:]
97+
98+
result = append(result, F(v))
99+
}
100+
101+
*(*[]F)(unsafe.Pointer(dst.UnsafeAddr())) = result
102+
103+
return true, nil
104+
}
105+
106+
func decodeFloat32(buf []byte, dst reflect.Value) (bool, error) {
107+
var result []float32
108+
109+
for len(buf) > 0 {
110+
v, n := protowire.ConsumeFixed32(buf)
111+
if n < 0 {
112+
return false, errors.New("bad protobuf 32-bit value")
113+
}
114+
115+
buf = buf[n:]
116+
117+
result = append(result, math.Float32frombits(v))
118+
}
119+
120+
*(*[]float32)(unsafe.Pointer(dst.UnsafeAddr())) = result
121+
122+
return true, nil
123+
}
124+
125+
func decodeFloat64(buf []byte, dst reflect.Value) (bool, error) {
126+
var result []float64
127+
128+
for len(buf) > 0 {
129+
v, n := protowire.ConsumeFixed64(buf)
130+
if n < 0 {
131+
return false, errors.New("bad protobuf 64-bit value")
132+
}
133+
134+
buf = buf[n:]
135+
136+
result = append(result, math.Float64frombits(v))
137+
}
138+
139+
*(*[]float64)(unsafe.Pointer(dst.UnsafeAddr())) = result
140+
141+
return true, nil
142+
}

0 commit comments

Comments
 (0)