Skip to content

Commit

Permalink
bytes (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikolaydubina authored Oct 26, 2023
1 parent 91732d0 commit 5ef9f0f
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 68 deletions.
98 changes: 61 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,63 +56,65 @@ It is wrapped into struct to prevent bugs:

Parse
```
$ go test -bench=. -benchtime=5s -benchmem ./...
$ go test -bench=BenchmarkParse -benchtime=5s -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpdecimal
BenchmarkParse/small-10 836845129 7.1 ns/op 0 B/op 0 allocs/op
BenchmarkParse/large-10 270274911 22.2 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_Atoi/small-10 1000000000 4.8 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_Atoi/large-10 415960243 14.3 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_ParseInt/small/int32-10 563149866 10.6 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_ParseInt/small/int64-10 568933998 10.5 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_ParseInt/large/int64-10 223803350 27.3 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/small/float32-10 342714165 17.6 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/small/float64-10 335826322 18.0 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/large/float32-10 124264724 48.1 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/large/float64-10 128111449 47.3 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_fmt_Sscanf/small-10 20766676 293.2 ns/op 69 B/op 2 allocs/op
BenchmarkParse_float_fmt_Sscanf/large-10 9707408 612.7 ns/op 88 B/op 3 allocs/op
BenchmarkParse/fromString/small-10 534307098 11.36 ns/op 0 B/op 0 allocs/op
BenchmarkParse/fromString/large-10 254741558 23.42 ns/op 0 B/op 0 allocs/op
BenchmarkParse/UnmarshalJSON/small-10 816873427 7.32 ns/op 0 B/op 0 allocs/op
BenchmarkParse/UnmarshalJSON/large-10 272173255 22.16 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_Atoi/small-10 1000000000 4.87 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_Atoi/large-10 420536834 14.31 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_ParseInt/small/int32-10 561137575 10.67 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_ParseInt/small/int64-10 564200026 10.64 ns/op 0 B/op 0 allocs/op
BenchmarkParse_int_strconv_ParseInt/large/int64-10 219626983 27.17 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/small/float32-10 345666214 17.36 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/small/float64-10 339620222 17.68 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/large/float32-10 128824344 46.68 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/large/float64-10 128140617 46.89 ns/op 0 B/op 0 allocs/op
BenchmarkParse_float_fmt_Sscanf/small-10 21202892 281.6 ns/op 69 B/op 2 allocs/op
BenchmarkParse_float_fmt_Sscanf/large-10 10074237 599.2 ns/op 88 B/op 3 allocs/op
PASS
ok github.com/nikolaydubina/fpdecimal 194.558s
ok github.com/nikolaydubina/fpdecimal 116.249s
```

Print
```
$ go test -bench=. -benchtime=5s -benchmem ./...
$ go test -bench=BenchmarkPrint -benchtime=5s -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpdecimal
BenchmarkPrint/small-10 214360207 28.1 ns/op 8 B/op 1 allocs/op
BenchmarkPrint/large-10 181972407 32.8 ns/op 24 B/op 1 allocs/op
BenchmarkPrint_int_strconv_Itoa/small-10 424602669 13.7 ns/op 3 B/op 0 allocs/op
BenchmarkPrint_int_strconv_Itoa/large-10 215629374 27.8 ns/op 18 B/op 1 allocs/op
BenchmarkPrint_int_strconv_FormatInt/small-10 428783829 13.7 ns/op 3 B/op 0 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/small/float32-10 56737408 106.0 ns/op 31 B/op 2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/small/float64-10 43639258 140.3 ns/op 31 B/op 2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/large/float32-10 63764750 96.5 ns/op 48 B/op 2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/large/float64-10 64598815 92.2 ns/op 48 B/op 2 allocs/op
BenchmarkPrint_float_fmt_Sprintf/small-10 45866606 131.7 ns/op 16 B/op 2 allocs/op
BenchmarkPrint_float_fmt_Sprintf/large-10 49536778 115.0 ns/op 28 B/op 2 allocs/op
BenchmarkPrint/small-10 191982066 31.24 ns/op 8 B/op 1 allocs/op
BenchmarkPrint/large-10 150874335 39.89 ns/op 24 B/op 1 allocs/op
BenchmarkPrint_int_strconv_Itoa/small-10 446302868 13.39 ns/op 3 B/op 0 allocs/op
BenchmarkPrint_int_strconv_Itoa/large-10 237484774 25.20 ns/op 18 B/op 1 allocs/op
BenchmarkPrint_int_strconv_FormatInt/small-10 444861666 13.70 ns/op 3 B/op 0 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/small/float32-10 55003357 104.2 ns/op 31 B/op 2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/small/float64-10 43565430 137.4 ns/op 31 B/op 2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/large/float32-10 64069650 92.07 ns/op 48 B/op 2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/large/float64-10 68441746 87.36 ns/op 48 B/op 2 allocs/op
BenchmarkPrint_float_fmt_Sprintf/small-10 46503666 127.7 ns/op 16 B/op 2 allocs/op
BenchmarkPrint_float_fmt_Sprintf/large-10 51764224 115.8 ns/op 28 B/op 2 allocs/op
PASS
ok github.com/nikolaydubina/fpdecimal 194.558s
ok github.com/nikolaydubina/fpdecimal 79.192s
```

Arithmetics
```
$ go test -bench=. -benchtime=5s -benchmem ./...
$ go test -bench=BenchmarkArithmetic -benchtime=5s -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpdecimal
BenchmarkArithmetic/add-10 1000000000 0.31 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic/div-10 962982672 0.84 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic/divmod-10 637345525 1.91 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/add-10 1000000000 0.31 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/div-10 1000000000 0.31 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/divmod-10 784951819 1.53 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/mod-10 1000000000 0.62 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic/add-10 1000000000 0.316 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic/div-10 1000000000 0.950 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic/divmod-10 1000000000 1.890 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/add-10 1000000000 0.314 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/div-10 1000000000 0.316 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/divmod-10 1000000000 1.261 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic_int64/mod-10 1000000000 0.628 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/nikolaydubina/fpdecimal 194.558s
ok github.com/nikolaydubina/fpdecimal 6.721s
```

## References
Expand Down Expand Up @@ -262,3 +264,25 @@ np.floor_divide(x, y) number // rounding down

Go does not support numerics in templates. However, defining multiple types each associated with specific number of decimals and passing them to functions and defining constraint as union of these types — is an attractive option.
This does not work well since Go does not support switch case (casting generic) back to integer well.

## Appendix F: `string` vs `[]byte` in interface

The typical usage of parsing number is through some JSON or other mechanism. Those APIs are dealing with `[]byte`.
Now, conversion from `[]byte` to `string` requires to copy data, since `string` is immutable.
To improve performance, we are using `[]byte` in signatures.

Using `string`
```
BenchmarkParse/fromString/small-10 831217767 7.07 ns/op 0 B/op 0 allocs/op
BenchmarkParse/fromString/large-10 275009497 21.79 ns/op 0 B/op 0 allocs/op
BenchmarkParse/UnmarshalJSON/small-10 553035127 10.98 ns/op 0 B/op 0 allocs/op
BenchmarkParse/UnmarshalJSON/large-10 248815030 24.14 ns/op 0 B/op 0 allocs/op
```

Using `[]byte`
```
BenchmarkParse/fromString/small-10 523937236 11.32 ns/op 0 B/op 0 allocs/op
BenchmarkParse/fromString/large-10 257542226 23.23 ns/op 0 B/op 0 allocs/op
BenchmarkParse/UnmarshalJSON/small-10 809793006 7.31 ns/op 0 B/op 0 allocs/op
BenchmarkParse/UnmarshalJSON/large-10 272087984 22.04 ns/op 0 B/op 0 allocs/op
```
8 changes: 4 additions & 4 deletions fpdecimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (a Decimal) Float32() float32 { return float32(a.v) / float32(multipliers[F

func (a Decimal) Float64() float64 { return float64(a.v) / float64(multipliers[FractionDigits]) }

func (a Decimal) String() string { return FixedPointDecimalToString(a.v, int(FractionDigits)) }
func (a Decimal) String() string { return FixedPointDecimalToString(a.v, FractionDigits) }

func (a Decimal) Add(b Decimal) Decimal { return Decimal{v: a.v + b.v} }

Expand All @@ -43,7 +43,7 @@ func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multipli

func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multipliers[FractionDigits] / b.v} }

func (a Decimal) DivMod(b Decimal) (part Decimal, remainder Decimal) {
func (a Decimal) DivMod(b Decimal) (part, remainder Decimal) {
// reduce divisor to avoid overflow of a at larger values
k := b.v / multipliers[FractionDigits]
return Decimal{v: a.v / k}, Decimal{v: a.v % k}
Expand All @@ -70,12 +70,12 @@ func (a Decimal) Compare(b Decimal) int {
}

func FromString(s string) (Decimal, error) {
v, err := ParseFixedPointDecimal(s, FractionDigits)
v, err := ParseFixedPointDecimal([]byte(s), FractionDigits)
return Decimal{v}, err
}

func (v *Decimal) UnmarshalJSON(b []byte) (err error) {
v.v, err = ParseFixedPointDecimal(string(b), FractionDigits)
v.v, err = ParseFixedPointDecimal(b, FractionDigits)
return err
}

Expand Down
66 changes: 51 additions & 15 deletions fpdecimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,38 @@ var floatsForTests = []struct {
func BenchmarkParse(b *testing.B) {
var s fp.Decimal
var err error
for _, tc := range floatsForTests {
b.Run(tc.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
s, err = fp.FromString(tc.vals[n%len(tc.vals)])
if err != nil || s == fp.Zero {
b.Error(s, err)

b.Run("fromString", func(b *testing.B) {
for _, tc := range floatsForTests {
b.ResetTimer()
b.Run(tc.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
s, err = fp.FromString(tc.vals[n%len(tc.vals)])
if err != nil || s == fp.Zero {
b.Error(s, err)
}
}
})
}
})

b.Run("UnmarshalJSON", func(b *testing.B) {
for _, tc := range floatsForTests {
var vals [][]byte
for i := range tc.vals {
vals = append(vals, []byte(tc.vals[i]))
}
})
}

b.ResetTimer()
b.Run(tc.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
if err = s.UnmarshalJSON(vals[n%len(vals)]); err != nil || s == fp.Zero {
b.Error(s, err)
}
}
})
}
})
}

func BenchmarkPrint(b *testing.B) {
Expand All @@ -259,14 +281,28 @@ func BenchmarkPrint(b *testing.B) {
tests = append(tests, fp.Zero.Sub(v))
}

b.ResetTimer()
b.Run(tc.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
s = tests[n%len(tc.vals)].String()
if s == "" {
b.Error("empty str")
b.Run("String", func(b *testing.B) {
b.ResetTimer()
b.Run(tc.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
s = tests[n%len(tc.vals)].String()
if s == "" {
b.Error("empty str")
}
}
}
})
})

b.Run("Marshal", func(b *testing.B) {
b.ResetTimer()
b.Run(tc.name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
s = tests[n%len(tc.vals)].String()
if s == "" {
b.Error("empty str")
}
}
})
})
}
}
Expand Down
6 changes: 3 additions & 3 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ var (
)

// ParseFixedPointDecimal parses fixed-point decimal of p fractions into int64.
func ParseFixedPointDecimal(s string, p uint8) (int64, error) {
if s == "" {
func ParseFixedPointDecimal(s []byte, p uint8) (int64, error) {
if len(s) == 0 {
return 0, errEmptyString
}

Expand All @@ -30,7 +30,7 @@ func ParseFixedPointDecimal(s string, p uint8) (int64, error) {
var pn = int8(p)
var d int8 = -1 // current decimal position
var n int64 // output
for _, ch := range []byte(s) {
for _, ch := range s {
if d == pn {
break
}
Expand Down
2 changes: 1 addition & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func FuzzParseFixedPointDecimal(f *testing.F) {
f.Add("-" + tc)
}
f.Fuzz(func(t *testing.T, s string) {
v, err := fpdecimal.ParseFixedPointDecimal(s, 3)
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), 3)
if err != nil {
if v != 0 {
t.Errorf("has to be 0 on error")
Expand Down
14 changes: 7 additions & 7 deletions printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
const zeroPrefix = "0.000000000000000000000000000000000000"

// FixedPointDecimalToString formats fixed-point decimal to string
func FixedPointDecimalToString(v int64, p int) string {
func FixedPointDecimalToString(v int64, p uint8) string {
// max int64: +9223372036854775.807
// min int64: -9223372036854775.808
// max bytes int64: 21
Expand All @@ -19,7 +19,7 @@ func FixedPointDecimalToString(v int64, p int) string {
// AppendFixedPointDecimal appends formatted fixed point decimal to destination buffer.
// Returns appended slice.
// This is efficient for avoiding memory copy.
func AppendFixedPointDecimal(b []byte, v int64, p int) []byte {
func AppendFixedPointDecimal(b []byte, v int64, p uint8) []byte {
if v == 0 {
return append(b, '0')
}
Expand All @@ -35,20 +35,20 @@ func AppendFixedPointDecimal(b []byte, v int64, p int) []byte {

// strconv.AppendInt is very efficient.
// Efficient converting int64 to ASCII is not as trivial.
s := len(b)
s := uint8(len(b))
b = strconv.AppendInt(b, v, 10)

// has whole?
if len(b)-s > p {
if uint8(len(b))-s > p {
// place decimal point
i := len(b) - p
i := uint8(len(b)) - p
b = append(b, 0)
copy(b[i+1:], b[i:])
b[i] = '.'
} else {
// append zeroes and decimal point
i := 2 + p - (len(b) - s)
for j := 0; j < i; j++ {
i := 2 + p - (uint8(len(b)) - s)
for j := uint8(0); j < i; j++ {
b = append(b, 0)
}
copy(b[s+i:], b[s:])
Expand Down
2 changes: 1 addition & 1 deletion printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func FuzzFixedPointDecimalToString(f *testing.F) {
s := fmt.Sprintf("%.3f", r)
rs, _ := strconv.ParseFloat(s, 64)

v, err := fpdecimal.ParseFixedPointDecimal(s, 3)
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), 3)
if err != nil {
t.Errorf(err.Error())
}
Expand Down

0 comments on commit 5ef9f0f

Please sign in to comment.