dtn7 / cboring

Go library for selected subset of CBOR's features
GNU General Public License v3.0
2 stars 2 forks source link

Is there any performance comparison with other CBOR libs? #2

Closed mogren closed 2 years ago

mogren commented 2 years ago

Just curious if there are any comparisons with other go CBOR implementations?

https://github.com/fxamacker/cbor/ for example.

Does the current implementation do everything needed?

oxzi commented 2 years ago

Thanks for your interest and please excuse the late reply; I am somewhat busy at moment.

Initially I wrote cboring as a CBOR library for dtn7-go, as the Bundle Protocol Version 7 makes some strange use of the CBOR specification. In BPv7, every structure is serialized as a CBOR array, where the array's length may vary. For an example, please check out the section about the Primary Block.

As it turned out, this was hard to realize with the go-code library, which tried to be smart and transform the data structure to the "obvious" CBOR object through the use of reflection with optional addition tags. It was also possible to write custom handlers for types, which I ended up doing, as the deserialization of arrays of custom length to structs was not really in the library's scope.

Back then I looked for other CBOR libraries, especially those who don't try to outsmart me and leave me the option to specify how data should be proceeded. Because I found no such library, I built cboring with the goals of being easy to use with custom serializations and being as small in code base as possible.

During the same time the fxamacker/cbor emerged, compare both initial commits: cboring, fxamacker/cbor. I only came across the other library some months later.

However, there seems to be a different scope of those two libraries. As go-codec, fxamacker/cbor library tries to work with assumptions. There is also an toarray attribute, but I have not tested how it behaves with different length.

For dtn7-go, the cbor library does everything that is necessary, as it implements a good part of the CBOR standard and enables the flexibility to mostly work with different CBOR arrays. For a more general use case, the fxamacker/cbor library or go-codec should be the better choice.

In a small, truly non-significant test, I briefly compared the two libraries in terms of encoding UInts.

$ go mod graph
github.com/oxzi/cbor-bench github.com/dtn7/cboring@v0.1.5
github.com/oxzi/cbor-bench github.com/fxamacker/cbor/v2@v2.4.0
github.com/fxamacker/cbor/v2@v2.4.0 github.com/x448/float16@v0.8.4

$ cat bench_uint_test.go
package main

import (
        "bytes"
        "testing"

        "github.com/dtn7/cboring"
        "github.com/fxamacker/cbor/v2"
)

func BenchmarkUintWriteCboring(b *testing.B) {
        var buff bytes.Buffer

        for i := 0; i < b.N; i++ {
                if err := cboring.WriteUInt(uint64(i), &buff); err != nil {
                        b.Fatal(err)
                }
        }
}

func BenchmarkUintWriteCbor(b *testing.B) {
        var buff bytes.Buffer
        var enc = cbor.NewEncoder(&buff)

        for i := 0; i < b.N; i++ {
                if err := enc.Encode(uint64(i)); err != nil {
                        b.Fatal(err)
                }
        }
}

$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/oxzi/cbor-bench
cpu: Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz
BenchmarkUintWriteCboring-4     13475816                82.24 ns/op
BenchmarkUintWriteCbor-4         9289813               122.3 ns/op
PASS
ok      github.com/oxzi/cbor-bench      2.480s
mogren commented 2 years ago

Thanks a lot for this long and detailed answer! Especially the link to the Primary Bundle Block and the code sample. I think you are totally right that cboring is the right implementation for dtn7-go. 😄

fxamacker commented 2 years ago

Hi! :wave: I'm sharing this to provide a more complete picture about encoding speed of uint64 with fxamacker/cbor.

StreamEncoder.EncodeUint64() is available the feature/stream-mode branch. It's fast and doesn't allocate memory:

name                     time/op        alloc/op      allocs/op
UintWriteCboring-8       33.7ns ± 3%    27.0B ± 0%       1
UintWriteCbor-8          69.1ns ± 0%    19.0B ± 0%       0
UintStreamWriteCbor-8    10.1ns ± 1%    16.0B ± 0%       0

* Go 1.17 linux_amd64 (Haswell Xeon)

The feature/stream-mode branch (superset of main) was created in April 2021. It's regularly fuzz tested and is used in production for nearly a year. However, stream-mode is meant for advanced users comfortable with CBOR.

It will be merged into main branch after adding any missing features and documenting new features.

@oxzi @mogren It's great to find kindred spirits who worked on CBOR around the same time. BTW, I created stream-mode while displaced (Winter Storm Uri made my place uninhabitable for 6 months).

Benchmark details

$ go test -bench=. -benchmem -benchtime=100000000x -count=10 | tee bench_cbor_encode_uint64_c10.log

(click to expand)...

``` goos: linux goarch: amd64 pkg: compare_cboring cpu: Intel(R) Xeon(R) CPU E3-1246 v3 @ 3.50GHz BenchmarkUintWriteCboring-8 100000000 34.64 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.51 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.65 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.57 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.57 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.60 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.94 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 36.43 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.67 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCboring-8 100000000 33.56 ns/op 27 B/op 1 allocs/op BenchmarkUintWriteCbor-8 100000000 69.15 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 69.15 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 69.12 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 70.85 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 69.14 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 69.09 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 69.35 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 70.40 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 69.17 ns/op 19 B/op 0 allocs/op BenchmarkUintWriteCbor-8 100000000 68.96 ns/op 19 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.24 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.04 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.09 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.12 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.06 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.05 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.36 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.12 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.03 ns/op 16 B/op 0 allocs/op BenchmarkUintStreamWriteCbor-8 100000000 10.11 ns/op 16 B/op 0 allocs/op PASS ok compare_cboring 114.223s ```

$ benchstat bench_cbor_encode_uint64_c10.log

(click to expand)...

``` benchstat bench_cbor_encode_uint64_c10.log name time/op UintWriteCboring-8 33.7ns ± 3% UintWriteCbor-8 69.1ns ± 0% UintStreamWriteCbor-8 10.1ns ± 1% name alloc/op UintWriteCboring-8 27.0B ± 0% UintWriteCbor-8 19.0B ± 0% UintStreamWriteCbor-8 16.0B ± 0% name allocs/op UintWriteCboring-8 1.00 ± 0% UintWriteCbor-8 0.00 UintStreamWriteCbor-8 0.00 ```

$ cat go.mod

(click to expand)...

``` module compare_cboring go 1.17 require ( [github.com/dtn7/cboring](http://github.com/dtn7/cboring) v0.1.5 // indirect [github.com/fxamacker/cbor/v2](http://github.com/fxamacker/cbor/v2) v2.3.1-0.20211029162100-5d5d7c3edd41 // indirect [github.com/x448/float16](http://github.com/x448/float16) v0.8.4 // indirect ) ```

$ cat bench_uint64_test.go

(click to expand)...

```Go package main import ( "bytes" "testing" "github.com/dtn7/cboring" "github.com/fxamacker/cbor/v2" ) func BenchmarkUintWriteCboring(b *testing.B) { var buff bytes.Buffer for i := 0; i < b.N; i++ { if err := cboring.WriteUInt(uint64(i), &buff); err != nil { b.Fatal(err) } } } func BenchmarkUintWriteCbor(b *testing.B) { var buff bytes.Buffer var enc = cbor.NewEncoder(&buff) for i := 0; i < b.N; i++ { if err := enc.Encode(uint64(i)); err != nil { b.Fatal(err) } } } // BenchmarkUintStreamWriteCbor requires feature/stream-mode branch // of github.com/fxamacker/cbor. func BenchmarkUintStreamWriteCbor(b *testing.B) { var buff bytes.Buffer var enc = cbor.NewStreamEncoder(&buff) for i := 0; i < b.N; i++ { if err := enc.EncodeUint64(uint64(i)); err != nil { b.Fatal(err) } } // Flush to make sure all previously encoded data is copied to buff. enc.Flush() } ```