Skip to content

Commit bce37a8

Browse files
authored
Add baseline_rw benchmark (#177)
* baseline: Add Reader/Writer variant This variant is meant to demonstrate and generate a baseline for serializers that can marshal/unmarshal to/from standard io Reader and Writer intefaces. While the decision on reusing auxiliary buffers is arbitrary, it is assumed that writing native types (uint64, etc) can be easily done by any Reader/Writer implementation itself (or their callers). The ultimate goal for this baseline test is as a benchmark against other implementations that offer this method of serialization, strictly measuring only the overhead of going through the io interfaces. Both safe and unsafe+reuse implementations are provided. * report: Update with baseline_rw results
1 parent 630ee59 commit bce37a8

5 files changed

Lines changed: 743 additions & 371 deletions

File tree

benchmarks.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,28 @@ var benchmarkCases = []BenchmarkCase{
502502
Notes: []string{
503503
"This is a manually written encoding, designed to be the fastest possible for this benchmark.",
504504
},
505+
}, {
506+
Name: "baseline_rw",
507+
URL: "",
508+
New: baseline.NewBaselineReaderWriter,
509+
510+
TimeSupport: TSNoSupport,
511+
APIKind: AKManual,
512+
Notes: []string{
513+
"This is a manually written encoding, designed to be the fastest possible for this benchmark, using an io.Reader/io.Writer as the API.",
514+
},
515+
}, {
516+
Name: "baseline_rw/unsafe_reuse",
517+
URL: "",
518+
New: baseline.NewBaselineReaderWriterUnsafeReuse,
519+
520+
UnsafeStringUnmarshal: true,
521+
BufferReuseMarshal: true,
522+
TimeSupport: TSNoSupport,
523+
APIKind: AKManual,
524+
Notes: []string{
525+
"This is a manually written encoding, designed to be the fastest possible for this benchmark, using an io.Reader/io.Writer as the API.",
526+
},
505527
}, {
506528
Name: "fastape",
507529
URL: "github.com/nazarifard/fastape",

internal/serializers/baseline/baseline.go

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/binary"
55
"math"
66
"time"
7-
"unsafe"
87

98
"github.com/alecthomas/go_serialization_benchmarks/goserbench"
109
)
@@ -17,28 +16,6 @@ import (
1716
// marshalling/unmarshalling operations.
1817
type BaselineSerializer struct{}
1918

20-
// maxSmallStructSerializeSize is the max size of a small struct serialized
21-
// with the baseline serializer.
22-
const maxSmallStructSerializeSize = 8 + 8 + 4 + 1 + goserbench.MaxSmallStructPhoneSize + goserbench.MaxSmallStructNameSize
23-
24-
// appendBool appends a bool to b.
25-
func appendBool(b []byte, v bool) []byte {
26-
if v {
27-
return append(b, 1)
28-
} else {
29-
return append(b, 0)
30-
}
31-
}
32-
33-
// getBool reads the next bool from b.
34-
func getBool(b []byte) bool {
35-
if b[0] == 0 {
36-
return false
37-
} else {
38-
return true
39-
}
40-
}
41-
4219
func (b *BaselineSerializer) Marshal(o interface{}) ([]byte, error) {
4320
a := o.(*goserbench.SmallStruct)
4421
buf := make([]byte, 0, maxSmallStructSerializeSize)
@@ -89,9 +66,8 @@ func (b *BaselineUnsafeSerializer) Unmarshal(d []byte, o interface{}) error {
8966
a.BirthDay = time.Unix(0, int64(binary.LittleEndian.Uint64(d[:8])))
9067
a.Money = math.Float64frombits(binary.LittleEndian.Uint64(d[8:16]))
9168
a.Siblings = int(binary.LittleEndian.Uint32(d[16:20]))
92-
nameSlice, phoneSlice := d[20:36], d[36:46]
93-
a.Name = *(*string)(unsafe.Pointer(&nameSlice))
94-
a.Phone = *(*string)(unsafe.Pointer(&phoneSlice))
69+
a.Name = unsafeSliceToString(d[20:36])
70+
a.Phone = unsafeSliceToString(d[36:46])
9571
a.Spouse = getBool(d[46:])
9672
return nil
9773
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package baseline
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"io"
7+
"math"
8+
"time"
9+
10+
"github.com/alecthomas/go_serialization_benchmarks/goserbench"
11+
)
12+
13+
// bytesMarshable is an interface for an io.Writer that can also generate a byte
14+
// slice and reset its size (i.e. a subset of *bytes.Buffer).
15+
type bytesMarshable interface {
16+
io.Writer
17+
Bytes() []byte
18+
Reset()
19+
}
20+
21+
// alwaysNilWriter is always nil. It is used to ensure the compiler does not
22+
// devirtualize calls to bytes.Buffer.Write().
23+
var alwaysNilWriter bytesMarshable
24+
25+
// awlaysNilReader is always nil. It is used to ensure the compiler does not
26+
// devirtualize calls to bytes.Reader.Read().
27+
var awlaysNilReader io.Reader
28+
29+
// BaselineReaderWriter is a hand-written baseline serializer that reads/writes
30+
// using standard io.Reader and io.Writer interfaces.
31+
//
32+
// While goserbench utlimately generates a []byte as a result of Marshal (and
33+
// reads from a []byte as well), this verifies the performance of using the
34+
// generic io interface, which could be used to write to other streams directly
35+
// (e.g. file or socket).
36+
type BaselineReaderWriter struct {
37+
// Store r to avoid this allocation. This is meant to simulate the fact
38+
// that Unmarshal() receives a Reader directly (instead of a []byte).
39+
r *bytes.Reader
40+
41+
// Store w to avoid this allocation. This is meant to simulate the fact
42+
// that Marshal() receives a Writer directly (instead of a []byte).
43+
w *simpleBufferWriter
44+
45+
// aux buffer to read native values (int64, etc). Assume that the caller
46+
// can amortize this allocation somehow.
47+
auxBuf []byte
48+
49+
// aux buffer to read strings.
50+
strBuf []byte
51+
}
52+
53+
func (b *BaselineReaderWriter) Marshal(o interface{}) ([]byte, error) {
54+
// Create a new bytes buffer. We do it like this (referencing
55+
// alwaysNilWriter first) to avoid the compiler de-virtualizing and
56+
// inlining the calls to w.Write() directly into bytes.Buffer.Write()
57+
// calls, which simulates the worst case scenario of the overhead in
58+
// using Write() calls.
59+
w := alwaysNilWriter
60+
if w == nil {
61+
// Create with the pre-sized buffer. We assume the caller will
62+
// be able to hint to the writer either the final size or at
63+
// least an upper bound of what will be written, and that the
64+
// writer will be able to use that hint (e.g. in case of a file,
65+
// it could grow the file to allow adding that many bytes,
66+
// create a kernel buffer, memory map the file, etc).
67+
b.w.b = make([]byte, 0, maxSmallStructSerializeSize)
68+
w = b.w
69+
}
70+
71+
a := o.(*goserbench.SmallStruct)
72+
extra := b.auxBuf[:0]
73+
if _, err := w.Write(binary.LittleEndian.AppendUint64(extra, uint64(a.BirthDay.UnixNano()))); err != nil {
74+
return nil, err
75+
}
76+
if _, err := w.Write(binary.LittleEndian.AppendUint64(extra, math.Float64bits(a.Money))); err != nil {
77+
return nil, err
78+
}
79+
if _, err := w.Write(binary.LittleEndian.AppendUint32(extra, uint32(a.Siblings))); err != nil {
80+
return nil, err
81+
}
82+
if _, err := w.Write([]byte(a.Name)); err != nil {
83+
return nil, err
84+
}
85+
if _, err := w.Write([]byte(a.Phone)); err != nil {
86+
return nil, err
87+
}
88+
if _, err := w.Write(boolToByteSlice(a.Spouse)); err != nil {
89+
return nil, err
90+
}
91+
92+
return w.Bytes(), nil
93+
}
94+
95+
func (b *BaselineReaderWriter) Unmarshal(d []byte, o interface{}) error {
96+
// Create reader with passed buffer. We do it like this (referencing
97+
// alwaysNilReader first) to ensure the compiler does not devirtualize
98+
// and inline the Read() calls direcly to the *bytes.Buffer.
99+
r := awlaysNilReader
100+
if r == nil {
101+
b.r.Reset(d)
102+
r = b.r
103+
}
104+
105+
a := o.(*goserbench.SmallStruct)
106+
aux := b.auxBuf
107+
strs := b.strBuf
108+
109+
if _, err := io.ReadFull(r, aux[:8]); err != nil {
110+
return err
111+
}
112+
a.BirthDay = time.Unix(0, int64(binary.LittleEndian.Uint64(aux)))
113+
114+
if _, err := io.ReadFull(r, aux[:8]); err != nil {
115+
return err
116+
}
117+
a.Money = math.Float64frombits(binary.LittleEndian.Uint64(aux))
118+
119+
if _, err := io.ReadFull(r, aux[:4]); err != nil {
120+
return err
121+
}
122+
a.Siblings = int(binary.LittleEndian.Uint32(aux))
123+
124+
if _, err := io.ReadFull(r, strs[:goserbench.MaxSmallStructNameSize]); err != nil {
125+
return err
126+
}
127+
a.Name = string(strs[:goserbench.MaxSmallStructNameSize])
128+
129+
if _, err := io.ReadFull(r, strs[:goserbench.MaxSmallStructPhoneSize]); err != nil {
130+
return err
131+
}
132+
a.Phone = string(strs[:goserbench.MaxSmallStructPhoneSize])
133+
134+
if _, err := io.ReadFull(r, aux[:1]); err != nil {
135+
return err
136+
}
137+
a.Spouse = aux[0] != 0
138+
return nil
139+
}
140+
141+
func NewBaselineReaderWriter() goserbench.Serializer {
142+
return &BaselineReaderWriter{
143+
r: bytes.NewReader(nil),
144+
w: &simpleBufferWriter{},
145+
auxBuf: make([]byte, 8),
146+
strBuf: make([]byte, max(goserbench.MaxSmallStructNameSize, goserbench.MaxSmallStructPhoneSize)),
147+
}
148+
}
149+
150+
// BaselineReaderWriterUnsafeReuse is a hand-written baseline serializer that
151+
// reads/writes using standard io.Reader and io.Writer interfaces. It reuses
152+
// buffers and unsafe strings.
153+
type BaselineReaderWriterUnsafeReuse struct {
154+
// w saves the buffer for writing.
155+
w bytesMarshable
156+
157+
// r avoids having to allocate a *bytes.Reader reference.
158+
r *bytes.Reader
159+
160+
// aux buffer to read native values (int64, etc).
161+
auxBuf []byte
162+
163+
// aux buffer to read strings.
164+
strBuf []byte
165+
}
166+
167+
func (b *BaselineReaderWriterUnsafeReuse) Marshal(o interface{}) ([]byte, error) {
168+
// Get a reference to the writer and reset its buffer. We do it like
169+
// this (referencing alwaysNilWriter first) to ensure the compiler does
170+
// not devirtualize and inline the Write() calls direcly to the
171+
// *bytes.Buffer.
172+
w := alwaysNilWriter
173+
if w == nil {
174+
w = b.w
175+
w.Reset()
176+
}
177+
178+
a := o.(*goserbench.SmallStruct)
179+
extra := b.auxBuf[:0]
180+
if _, err := w.Write(binary.LittleEndian.AppendUint64(extra, uint64(a.BirthDay.UnixNano()))); err != nil {
181+
return nil, err
182+
}
183+
if _, err := w.Write(binary.LittleEndian.AppendUint64(extra, math.Float64bits(a.Money))); err != nil {
184+
return nil, err
185+
}
186+
if _, err := w.Write(binary.LittleEndian.AppendUint32(extra, uint32(a.Siblings))); err != nil {
187+
return nil, err
188+
}
189+
if _, err := w.Write(unsafeStringToSlice(a.Name)); err != nil {
190+
return nil, err
191+
}
192+
if _, err := w.Write(unsafeStringToSlice(a.Phone)); err != nil {
193+
return nil, err
194+
}
195+
if _, err := w.Write(boolToByteSlice(a.Spouse)); err != nil {
196+
return nil, err
197+
}
198+
199+
return w.Bytes(), nil
200+
}
201+
202+
func (b *BaselineReaderWriterUnsafeReuse) Unmarshal(d []byte, o interface{}) error {
203+
// Reset reader to passed buffer. We do it like this (referencing
204+
// alwaysNilReader first) to ensure the compiler does not devirtualize
205+
// and inline the Read() calls direcly to the *bytes.Buffer.
206+
r := awlaysNilReader
207+
if r == nil {
208+
b.r.Reset(d)
209+
r = b.r
210+
}
211+
212+
a := o.(*goserbench.SmallStruct)
213+
aux := b.auxBuf
214+
strs := b.strBuf
215+
216+
if _, err := io.ReadFull(r, aux[:8]); err != nil {
217+
return err
218+
}
219+
a.BirthDay = time.Unix(0, int64(binary.LittleEndian.Uint64(aux)))
220+
221+
if _, err := io.ReadFull(r, aux[:8]); err != nil {
222+
return err
223+
}
224+
a.Money = math.Float64frombits(binary.LittleEndian.Uint64(aux))
225+
226+
if _, err := io.ReadFull(r, aux[:4]); err != nil {
227+
return err
228+
}
229+
a.Siblings = int(binary.LittleEndian.Uint32(aux))
230+
231+
if _, err := io.ReadFull(r, strs[:goserbench.MaxSmallStructNameSize]); err != nil {
232+
return err
233+
}
234+
a.Name = unsafeSliceToString(strs[:goserbench.MaxSmallStructNameSize])
235+
strs = strs[goserbench.MaxSmallStructNameSize:]
236+
237+
if _, err := io.ReadFull(r, strs[:goserbench.MaxSmallStructPhoneSize]); err != nil {
238+
return err
239+
}
240+
a.Phone = unsafeSliceToString(strs[:goserbench.MaxSmallStructPhoneSize])
241+
242+
if _, err := io.ReadFull(r, aux[:1]); err != nil {
243+
return err
244+
}
245+
a.Spouse = aux[0] != 0
246+
return nil
247+
}
248+
249+
func NewBaselineReaderWriterUnsafeReuse() goserbench.Serializer {
250+
return &BaselineReaderWriterUnsafeReuse{
251+
w: bytes.NewBuffer(make([]byte, 0, maxSmallStructSerializeSize)),
252+
r: bytes.NewReader(nil),
253+
auxBuf: make([]byte, 8),
254+
strBuf: make([]byte, goserbench.MaxSmallStructNameSize+goserbench.MaxSmallStructPhoneSize),
255+
}
256+
}

0 commit comments

Comments
 (0)