From d872dc9ad814e00d9523c8c534b2b350dcaca7b0 Mon Sep 17 00:00:00 2001 From: Gab Virebent Date: Thu, 27 Aug 2026 23:38:23 +0200 Subject: [PATCH] Optimize unconstrained byte array serialization --- gen/decl.go | 51 +++++++++++++++++++++++++ gen/tests/contexts/gen-marshallers.go | 22 +++-------- gen/tests/fixie/gen-marshallers.go | 32 +++++----------- gen/tests/rem/benchmark_test.go | 27 +++++++++++++ gen/tests/rem/gen-marshallers.go | 19 ++------- gen/tests/rem/rem_test.go | 9 +++++ gen/tests/unionbasic/gen-marshallers.go | 16 +++----- gen/tests/unionlen/gen-marshallers.go | 19 ++------- gen/tests/unionlo/gen-marshallers.go | 34 ++++------------- test_bytes_field/gen-marshallers.go | 16 +++----- 10 files changed, 127 insertions(+), 118 deletions(-) create mode 100644 gen/tests/rem/benchmark_test.go diff --git a/gen/decl.go b/gen/decl.go index ab91862..0fe2781 100644 --- a/gen/decl.go +++ b/gen/decl.go @@ -249,6 +249,30 @@ func (g *generator) parseIntType(lhs string, t *ast.IntType) { } func (g *generator) parseArray(lhs string, base ast.Type, s ast.LengthConstraint) { + if unconstrainedByteType(base) { + switch s := s.(type) { + case *ast.IntegerConstRef, *ast.IntegerLiteral: + size := g.integer(s) + g.lengthCheck(size) + g.printf("copy(%s[:], %s[:%s])\n", lhs, g.data, size) + g.printf("%s = %s[%s:]\n", g.data, g.data, size) + return + case *ast.IDRef: + ref := g.ref(s) + g.printf("if uint64(%s) > uint64(len(%s)) { return nil, errors.New(\"data too short\") }\n", ref, g.data) + size := fmt.Sprintf("int(%s)", ref) + g.printf("%s = make([]%s, %s)\n", lhs, g.tipe(base), size) + g.printf("copy(%s, %s[:%s])\n", lhs, g.data, size) + g.printf("%s = %s[%s:]\n", g.data, g.data, size) + return + case nil: + g.printf("%s = make([]%s, len(%s))\n", lhs, g.tipe(base), g.data) + g.printf("copy(%s, %s)\n", lhs, g.data) + g.printf("%s = %s[len(%s):]\n", g.data, g.data, g.data) + return + } + } + switch s := s.(type) { case *ast.IntegerConstRef, *ast.IntegerLiteral: g.printf("for idx := 0; idx < %s; idx++ {\n", g.integer(s)) @@ -382,6 +406,19 @@ func elementByteSize(base ast.Type) int { } } +// unconstrainedByteType reports whether base can be copied without skipping +// per-element validation. +func unconstrainedByteType(base ast.Type) bool { + switch t := base.(type) { + case *ast.IntType: + return t.Size == 8 && t.Constraint == nil + case *ast.CharType: + return true + default: + return false + } +} + func (g *generator) integer(i ast.Integer) string { x, err := g.resolver.Integer(i) if err != nil { @@ -537,6 +574,16 @@ func (g *generator) encodeIntType(rhs string, t *ast.IntType) { } func (g *generator) encodeArray(rhs string, base ast.Type, s ast.LengthConstraint) { + if unconstrainedByteType(base) { + switch s.(type) { + case *ast.IntegerConstRef, *ast.IntegerLiteral: + g.printf("%s = append(%s, %s[:]...)\n", g.data, g.data, rhs) + default: + g.printf("%s = append(%s, %s...)\n", g.data, g.data, rhs) + } + return + } + switch s := s.(type) { case *ast.IntegerConstRef, *ast.IntegerLiteral: g.printf("for idx := 0; idx < %s; idx++ {\n", g.integer(s)) @@ -662,6 +709,10 @@ func (g *generator) validateArray(rhs string, base ast.Type, s ast.LengthConstra panic(unexpected(s)) } + if unconstrainedByteType(base) { + return + } + // Validate array elements g.printf("for idx := 0; idx < len(%s); idx++ {\n", rhs) g.validateType(rhs+"[idx]", base) diff --git a/gen/tests/contexts/gen-marshallers.go b/gen/tests/contexts/gen-marshallers.go index 8c4c179..f91fec3 100644 --- a/gen/tests/contexts/gen-marshallers.go +++ b/gen/tests/contexts/gen-marshallers.go @@ -144,13 +144,8 @@ func (v *Varsize) Parse(data []byte, count Count) ([]byte, error) { return nil, errors.New("data too short") } v.Msg = make([]uint8, int(count.Countval)) - for idx := 0; idx < int(count.Countval); idx++ { - if len(cur) < 1 { - return nil, errors.New("data too short") - } - v.Msg[idx] = cur[0] - cur = cur[1:] - } + copy(v.Msg, cur[:int(count.Countval)]) + cur = cur[int(count.Countval):] } return cur, nil } @@ -210,16 +205,9 @@ func (c *Ccomplex) Parse(data []byte, flag Flag, count Count) ([]byte, error) { switch { case flag.Flagval == 0: { - c.A = make([]uint8, 0) - for len(cur) > 0 { - var tmp uint8 - if len(cur) < 1 { - return nil, errors.New("data too short") - } - tmp = cur[0] - cur = cur[1:] - c.A = append(c.A, tmp) - } + c.A = make([]uint8, len(cur)) + copy(c.A, cur) + cur = cur[len(cur):] } case flag.Flagval == 1: { diff --git a/gen/tests/fixie/gen-marshallers.go b/gen/tests/fixie/gen-marshallers.go index 89c4cbc..2b9d2b7 100644 --- a/gen/tests/fixie/gen-marshallers.go +++ b/gen/tests/fixie/gen-marshallers.go @@ -87,22 +87,18 @@ type FixieDemo struct { func (f *FixieDemo) Parse(data []byte) ([]byte, error) { cur := data { - for idx := 0; idx < 8; idx++ { - if len(cur) < 1 { - return nil, errors.New("data too short") - } - f.Bytes[idx] = cur[0] - cur = cur[1:] + if len(cur) < 8 { + return nil, errors.New("data too short") } + copy(f.Bytes[:], cur[:8]) + cur = cur[8:] } { - for idx := 0; idx < 8; idx++ { - if len(cur) < 1 { - return nil, errors.New("data too short") - } - f.Letters[idx] = cur[0] - cur = cur[1:] + if len(cur) < 8 { + return nil, errors.New("data too short") } + copy(f.Letters[:], cur[:8]) + cur = cur[8:] } { for idx := 0; idx < 4; idx++ { @@ -158,12 +154,8 @@ func ParseFixieDemo(data []byte) (*FixieDemo, error) { func (f *FixieDemo) encodeBinary() []byte { var buf []byte - for idx := 0; idx < 8; idx++ { - buf = append(buf, byte(f.Bytes[idx])) - } - for idx := 0; idx < 8; idx++ { - buf = append(buf, byte(f.Letters[idx])) - } + buf = append(buf, f.Bytes[:]...) + buf = append(buf, f.Letters[:]...) for idx := 0; idx < 4; idx++ { { tmp := make([]byte, 2) @@ -204,13 +196,9 @@ func (f *FixieDemo) validate() error { if len(f.Bytes) != 8 { return errors.New("array length constraint violated") } - for idx := 0; idx < len(f.Bytes); idx++ { - } if len(f.Letters) != 8 { return errors.New("array length constraint violated") } - for idx := 0; idx < len(f.Letters); idx++ { - } if len(f.Shortwords) != 4 { return errors.New("array length constraint violated") } diff --git a/gen/tests/rem/benchmark_test.go b/gen/tests/rem/benchmark_test.go new file mode 100644 index 0000000..dbc1163 --- /dev/null +++ b/gen/tests/rem/benchmark_test.go @@ -0,0 +1,27 @@ +package rem + +import "testing" + +func BenchmarkParseByteTail(b *testing.B) { + data := make([]byte, 4+64*1024) + b.SetBytes(int64(len(data))) + b.ReportAllocs() + b.ResetTimer() + for range b.N { + if _, err := ParseRem(data); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkMarshalByteTail(b *testing.B) { + message := &Rem{Tail: make([]byte, 64*1024)} + b.SetBytes(int64(4 + len(message.Tail))) + b.ReportAllocs() + b.ResetTimer() + for range b.N { + if _, err := message.MarshalBinary(); err != nil { + b.Fatal(err) + } + } +} diff --git a/gen/tests/rem/gen-marshallers.go b/gen/tests/rem/gen-marshallers.go index 4fa9955..c8b072e 100644 --- a/gen/tests/rem/gen-marshallers.go +++ b/gen/tests/rem/gen-marshallers.go @@ -27,16 +27,9 @@ func (r *Rem) Parse(data []byte) ([]byte, error) { cur = cur[4:] } { - r.Tail = make([]uint8, 0) - for len(cur) > 0 { - var tmp uint8 - if len(cur) < 1 { - return nil, errors.New("data too short") - } - tmp = cur[0] - cur = cur[1:] - r.Tail = append(r.Tail, tmp) - } + r.Tail = make([]uint8, len(cur)) + copy(r.Tail, cur) + cur = cur[len(cur):] } return cur, nil } @@ -60,9 +53,7 @@ func (r *Rem) encodeBinary() []byte { binary.BigEndian.PutUint32(tmp, r.Head) buf = append(buf, tmp...) } - for idx := 0; idx < len(r.Tail); idx++ { - buf = append(buf, byte(r.Tail[idx])) - } + buf = append(buf, r.Tail...) return buf } @@ -74,7 +65,5 @@ func (r *Rem) MarshalBinary() ([]byte, error) { } func (r *Rem) validate() error { - for idx := 0; idx < len(r.Tail); idx++ { - } return nil } diff --git a/gen/tests/rem/rem_test.go b/gen/tests/rem/rem_test.go index 521dec6..d84c19d 100644 --- a/gen/tests/rem/rem_test.go +++ b/gen/tests/rem/rem_test.go @@ -54,3 +54,12 @@ func TestRemParseSuccess(t *testing.T) { Tail: []byte("thetail"), }, r) } + +func TestRemParseCopiesTail(t *testing.T) { + b := []byte{0, 1, 2, 3, 4, 5, 6} + r, err := ParseRem(b) + require.NoError(t, err) + + b[4] = 99 + assert.Equal(t, []byte{4, 5, 6}, r.Tail) +} diff --git a/gen/tests/unionbasic/gen-marshallers.go b/gen/tests/unionbasic/gen-marshallers.go index 4a72295..b053afe 100644 --- a/gen/tests/unionbasic/gen-marshallers.go +++ b/gen/tests/unionbasic/gen-marshallers.go @@ -121,13 +121,11 @@ func (b *Basic) Parse(data []byte) ([]byte, error) { } case b.Tag == 4: { - for idx := 0; idx < 8; idx++ { - if len(cur) < 1 { - return nil, errors.New("data too short") - } - b.Eightbytes[idx] = cur[0] - cur = cur[1:] + if len(cur) < 8 { + return nil, errors.New("data too short") } + copy(b.Eightbytes[:], cur[:8]) + cur = cur[8:] } case b.Tag == 6: { @@ -169,9 +167,7 @@ func (b *Basic) encodeBinary() []byte { buf = append(buf, tmp...) } case b.Tag == 4: - for idx := 0; idx < 8; idx++ { - buf = append(buf, byte(b.Eightbytes[idx])) - } + buf = append(buf, b.Eightbytes[:]...) case b.Tag == 6: buf = append(buf, []byte(b.String)...) buf = append(buf, 0) @@ -202,8 +198,6 @@ func (b *Basic) validate() error { if len(b.Eightbytes) != 8 { return errors.New("array length constraint violated") } - for idx := 0; idx < len(b.Eightbytes); idx++ { - } case b.Tag == 6: } return nil diff --git a/gen/tests/unionlen/gen-marshallers.go b/gen/tests/unionlen/gen-marshallers.go index e9305a1..9586859 100644 --- a/gen/tests/unionlen/gen-marshallers.go +++ b/gen/tests/unionlen/gen-marshallers.go @@ -97,16 +97,9 @@ func (u *UnionWithLen) Parse(data []byte) ([]byte, error) { } default: { - u.Unparseable = make([]uint8, 0) - for len(cur) > 0 { - var tmp uint8 - if len(cur) < 1 { - return nil, errors.New("data too short") - } - tmp = cur[0] - cur = cur[1:] - u.Unparseable = append(u.Unparseable, tmp) - } + u.Unparseable = make([]uint8, len(cur)) + copy(u.Unparseable, cur) + cur = cur[len(cur):] } } if len(cur) > 0 { @@ -162,9 +155,7 @@ func (u *UnionWithLen) encodeBinary() []byte { buf = append(buf, byte(u.Month)) buf = append(buf, byte(u.Day)) default: - for idx := 0; idx < len(u.Unparseable); idx++ { - buf = append(buf, byte(u.Unparseable[idx])) - } + buf = append(buf, u.Unparseable...) } { tmp := make([]byte, 2) @@ -186,8 +177,6 @@ func (u *UnionWithLen) validate() error { case u.Tag == 1: case u.Tag == 2: default: - for idx := 0; idx < len(u.Unparseable); idx++ { - } } return nil } diff --git a/gen/tests/unionlo/gen-marshallers.go b/gen/tests/unionlo/gen-marshallers.go index 7e2d8e4..f9bb332 100644 --- a/gen/tests/unionlo/gen-marshallers.go +++ b/gen/tests/unionlo/gen-marshallers.go @@ -48,16 +48,9 @@ func (u *Unlo) Parse(data []byte) ([]byte, error) { } case u.Tag == 2: { - u.Y = make([]uint8, 0) - for len(cur) > 0 { - var tmp uint8 - if len(cur) < 1 { - return nil, errors.New("data too short") - } - tmp = cur[0] - cur = cur[1:] - u.Y = append(u.Y, tmp) - } + u.Y = make([]uint8, len(cur)) + copy(u.Y, cur) + cur = cur[len(cur):] } case u.Tag == 4: { @@ -97,13 +90,8 @@ func (u *Unlo) Parse(data []byte) ([]byte, error) { return nil, errors.New("data too short") } u.Leftovers = make([]uint8, int(u.Leftoverlen)) - for idx := 0; idx < int(u.Leftoverlen); idx++ { - if len(cur) < 1 { - return nil, errors.New("data too short") - } - u.Leftovers[idx] = cur[0] - cur = cur[1:] - } + copy(u.Leftovers, cur[:int(u.Leftoverlen)]) + cur = cur[int(u.Leftoverlen):] } return cur, nil } @@ -127,9 +115,7 @@ func (u *Unlo) encodeBinary() []byte { case u.Tag == 1: buf = append(buf, byte(u.X)) case u.Tag == 2: - for idx := 0; idx < len(u.Y); idx++ { - buf = append(buf, byte(u.Y[idx])) - } + buf = append(buf, u.Y...) case u.Tag == 4: buf = append(buf, byte(u.Byte)) for idx := 0; idx < len(u.Z); idx++ { @@ -141,9 +127,7 @@ func (u *Unlo) encodeBinary() []byte { } } buf = append(buf, byte(u.Leftoverlen)) - for idx := 0; idx < int(u.Leftoverlen); idx++ { - buf = append(buf, byte(u.Leftovers[idx])) - } + buf = append(buf, u.Leftovers...) return buf } @@ -158,8 +142,6 @@ func (u *Unlo) validate() error { switch { case u.Tag == 1: case u.Tag == 2: - for idx := 0; idx < len(u.Y); idx++ { - } case u.Tag == 4: for idx := 0; idx < len(u.Z); idx++ { } @@ -167,7 +149,5 @@ func (u *Unlo) validate() error { if len(u.Leftovers) != int(u.Leftoverlen) { return errors.New("array length constraint violated") } - for idx := 0; idx < len(u.Leftovers); idx++ { - } return nil } diff --git a/test_bytes_field/gen-marshallers.go b/test_bytes_field/gen-marshallers.go index 8886aa4..1c7abde 100644 --- a/test_bytes_field/gen-marshallers.go +++ b/test_bytes_field/gen-marshallers.go @@ -11,13 +11,11 @@ type IRecv struct { func (i *IRecv) Parse(data []byte) ([]byte, error) { cur := data { - for idx := 0; idx < 8; idx++ { - if len(cur) < 1 { - return nil, errors.New("data too short") - } - i.Bytes[idx] = cur[0] - cur = cur[1:] + if len(cur) < 8 { + return nil, errors.New("data too short") } + copy(i.Bytes[:], cur[:8]) + cur = cur[8:] } return cur, nil } @@ -33,9 +31,7 @@ func ParseIRecv(data []byte) (*IRecv, error) { func (i *IRecv) encodeBinary() []byte { var buf []byte - for idx := 0; idx < 8; idx++ { - buf = append(buf, byte(i.Bytes[idx])) - } + buf = append(buf, i.Bytes[:]...) return buf } @@ -50,7 +46,5 @@ func (i *IRecv) validate() error { if len(i.Bytes) != 8 { return errors.New("array length constraint violated") } - for idx := 0; idx < len(i.Bytes); idx++ { - } return nil }