Skip to content

Commit 5ef9f0f

Browse files
bytes (#23)
1 parent 91732d0 commit 5ef9f0f

7 files changed

Lines changed: 128 additions & 68 deletions

File tree

README.md

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -56,63 +56,65 @@ It is wrapped into struct to prevent bugs:
5656

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

8082
Print
8183
```
82-
$ go test -bench=. -benchtime=5s -benchmem ./...
84+
$ go test -bench=BenchmarkPrint -benchtime=5s -benchmem .
8385
goos: darwin
8486
goarch: arm64
8587
pkg: github.com/nikolaydubina/fpdecimal
86-
BenchmarkPrint/small-10 214360207 28.1 ns/op 8 B/op 1 allocs/op
87-
BenchmarkPrint/large-10 181972407 32.8 ns/op 24 B/op 1 allocs/op
88-
BenchmarkPrint_int_strconv_Itoa/small-10 424602669 13.7 ns/op 3 B/op 0 allocs/op
89-
BenchmarkPrint_int_strconv_Itoa/large-10 215629374 27.8 ns/op 18 B/op 1 allocs/op
90-
BenchmarkPrint_int_strconv_FormatInt/small-10 428783829 13.7 ns/op 3 B/op 0 allocs/op
91-
BenchmarkPrint_float_strconv_FormatFloat/small/float32-10 56737408 106.0 ns/op 31 B/op 2 allocs/op
92-
BenchmarkPrint_float_strconv_FormatFloat/small/float64-10 43639258 140.3 ns/op 31 B/op 2 allocs/op
93-
BenchmarkPrint_float_strconv_FormatFloat/large/float32-10 63764750 96.5 ns/op 48 B/op 2 allocs/op
94-
BenchmarkPrint_float_strconv_FormatFloat/large/float64-10 64598815 92.2 ns/op 48 B/op 2 allocs/op
95-
BenchmarkPrint_float_fmt_Sprintf/small-10 45866606 131.7 ns/op 16 B/op 2 allocs/op
96-
BenchmarkPrint_float_fmt_Sprintf/large-10 49536778 115.0 ns/op 28 B/op 2 allocs/op
88+
BenchmarkPrint/small-10 191982066 31.24 ns/op 8 B/op 1 allocs/op
89+
BenchmarkPrint/large-10 150874335 39.89 ns/op 24 B/op 1 allocs/op
90+
BenchmarkPrint_int_strconv_Itoa/small-10 446302868 13.39 ns/op 3 B/op 0 allocs/op
91+
BenchmarkPrint_int_strconv_Itoa/large-10 237484774 25.20 ns/op 18 B/op 1 allocs/op
92+
BenchmarkPrint_int_strconv_FormatInt/small-10 444861666 13.70 ns/op 3 B/op 0 allocs/op
93+
BenchmarkPrint_float_strconv_FormatFloat/small/float32-10 55003357 104.2 ns/op 31 B/op 2 allocs/op
94+
BenchmarkPrint_float_strconv_FormatFloat/small/float64-10 43565430 137.4 ns/op 31 B/op 2 allocs/op
95+
BenchmarkPrint_float_strconv_FormatFloat/large/float32-10 64069650 92.07 ns/op 48 B/op 2 allocs/op
96+
BenchmarkPrint_float_strconv_FormatFloat/large/float64-10 68441746 87.36 ns/op 48 B/op 2 allocs/op
97+
BenchmarkPrint_float_fmt_Sprintf/small-10 46503666 127.7 ns/op 16 B/op 2 allocs/op
98+
BenchmarkPrint_float_fmt_Sprintf/large-10 51764224 115.8 ns/op 28 B/op 2 allocs/op
9799
PASS
98-
ok github.com/nikolaydubina/fpdecimal 194.558s
100+
ok github.com/nikolaydubina/fpdecimal 79.192s
99101
```
100102

101103
Arithmetics
102104
```
103-
$ go test -bench=. -benchtime=5s -benchmem ./...
105+
$ go test -bench=BenchmarkArithmetic -benchtime=5s -benchmem .
104106
goos: darwin
105107
goarch: arm64
106108
pkg: github.com/nikolaydubina/fpdecimal
107-
BenchmarkArithmetic/add-10 1000000000 0.31 ns/op 0 B/op 0 allocs/op
108-
BenchmarkArithmetic/div-10 962982672 0.84 ns/op 0 B/op 0 allocs/op
109-
BenchmarkArithmetic/divmod-10 637345525 1.91 ns/op 0 B/op 0 allocs/op
110-
BenchmarkArithmetic_int64/add-10 1000000000 0.31 ns/op 0 B/op 0 allocs/op
111-
BenchmarkArithmetic_int64/div-10 1000000000 0.31 ns/op 0 B/op 0 allocs/op
112-
BenchmarkArithmetic_int64/divmod-10 784951819 1.53 ns/op 0 B/op 0 allocs/op
113-
BenchmarkArithmetic_int64/mod-10 1000000000 0.62 ns/op 0 B/op 0 allocs/op
109+
BenchmarkArithmetic/add-10 1000000000 0.316 ns/op 0 B/op 0 allocs/op
110+
BenchmarkArithmetic/div-10 1000000000 0.950 ns/op 0 B/op 0 allocs/op
111+
BenchmarkArithmetic/divmod-10 1000000000 1.890 ns/op 0 B/op 0 allocs/op
112+
BenchmarkArithmetic_int64/add-10 1000000000 0.314 ns/op 0 B/op 0 allocs/op
113+
BenchmarkArithmetic_int64/div-10 1000000000 0.316 ns/op 0 B/op 0 allocs/op
114+
BenchmarkArithmetic_int64/divmod-10 1000000000 1.261 ns/op 0 B/op 0 allocs/op
115+
BenchmarkArithmetic_int64/mod-10 1000000000 0.628 ns/op 0 B/op 0 allocs/op
114116
PASS
115-
ok github.com/nikolaydubina/fpdecimal 194.558s
117+
ok github.com/nikolaydubina/fpdecimal 6.721s
116118
```
117119

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

263265
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.
264266
This does not work well since Go does not support switch case (casting generic) back to integer well.
267+
268+
## Appendix F: `string` vs `[]byte` in interface
269+
270+
The typical usage of parsing number is through some JSON or other mechanism. Those APIs are dealing with `[]byte`.
271+
Now, conversion from `[]byte` to `string` requires to copy data, since `string` is immutable.
272+
To improve performance, we are using `[]byte` in signatures.
273+
274+
Using `string`
275+
```
276+
BenchmarkParse/fromString/small-10 831217767 7.07 ns/op 0 B/op 0 allocs/op
277+
BenchmarkParse/fromString/large-10 275009497 21.79 ns/op 0 B/op 0 allocs/op
278+
BenchmarkParse/UnmarshalJSON/small-10 553035127 10.98 ns/op 0 B/op 0 allocs/op
279+
BenchmarkParse/UnmarshalJSON/large-10 248815030 24.14 ns/op 0 B/op 0 allocs/op
280+
```
281+
282+
Using `[]byte`
283+
```
284+
BenchmarkParse/fromString/small-10 523937236 11.32 ns/op 0 B/op 0 allocs/op
285+
BenchmarkParse/fromString/large-10 257542226 23.23 ns/op 0 B/op 0 allocs/op
286+
BenchmarkParse/UnmarshalJSON/small-10 809793006 7.31 ns/op 0 B/op 0 allocs/op
287+
BenchmarkParse/UnmarshalJSON/large-10 272087984 22.04 ns/op 0 B/op 0 allocs/op
288+
```

fpdecimal.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (a Decimal) Float32() float32 { return float32(a.v) / float32(multipliers[F
3333

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

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

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

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

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

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

7272
func FromString(s string) (Decimal, error) {
73-
v, err := ParseFixedPointDecimal(s, FractionDigits)
73+
v, err := ParseFixedPointDecimal([]byte(s), FractionDigits)
7474
return Decimal{v}, err
7575
}
7676

7777
func (v *Decimal) UnmarshalJSON(b []byte) (err error) {
78-
v.v, err = ParseFixedPointDecimal(string(b), FractionDigits)
78+
v.v, err = ParseFixedPointDecimal(b, FractionDigits)
7979
return err
8080
}
8181

fpdecimal_test.go

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,38 @@ var floatsForTests = []struct {
234234
func BenchmarkParse(b *testing.B) {
235235
var s fp.Decimal
236236
var err error
237-
for _, tc := range floatsForTests {
238-
b.Run(tc.name, func(b *testing.B) {
239-
for n := 0; n < b.N; n++ {
240-
s, err = fp.FromString(tc.vals[n%len(tc.vals)])
241-
if err != nil || s == fp.Zero {
242-
b.Error(s, err)
237+
238+
b.Run("fromString", func(b *testing.B) {
239+
for _, tc := range floatsForTests {
240+
b.ResetTimer()
241+
b.Run(tc.name, func(b *testing.B) {
242+
for n := 0; n < b.N; n++ {
243+
s, err = fp.FromString(tc.vals[n%len(tc.vals)])
244+
if err != nil || s == fp.Zero {
245+
b.Error(s, err)
246+
}
243247
}
248+
})
249+
}
250+
})
251+
252+
b.Run("UnmarshalJSON", func(b *testing.B) {
253+
for _, tc := range floatsForTests {
254+
var vals [][]byte
255+
for i := range tc.vals {
256+
vals = append(vals, []byte(tc.vals[i]))
244257
}
245-
})
246-
}
258+
259+
b.ResetTimer()
260+
b.Run(tc.name, func(b *testing.B) {
261+
for n := 0; n < b.N; n++ {
262+
if err = s.UnmarshalJSON(vals[n%len(vals)]); err != nil || s == fp.Zero {
263+
b.Error(s, err)
264+
}
265+
}
266+
})
267+
}
268+
})
247269
}
248270

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

262-
b.ResetTimer()
263-
b.Run(tc.name, func(b *testing.B) {
264-
for n := 0; n < b.N; n++ {
265-
s = tests[n%len(tc.vals)].String()
266-
if s == "" {
267-
b.Error("empty str")
284+
b.Run("String", func(b *testing.B) {
285+
b.ResetTimer()
286+
b.Run(tc.name, func(b *testing.B) {
287+
for n := 0; n < b.N; n++ {
288+
s = tests[n%len(tc.vals)].String()
289+
if s == "" {
290+
b.Error("empty str")
291+
}
268292
}
269-
}
293+
})
294+
})
295+
296+
b.Run("Marshal", func(b *testing.B) {
297+
b.ResetTimer()
298+
b.Run(tc.name, func(b *testing.B) {
299+
for n := 0; n < b.N; n++ {
300+
s = tests[n%len(tc.vals)].String()
301+
if s == "" {
302+
b.Error("empty str")
303+
}
304+
}
305+
})
270306
})
271307
}
272308
}

parser.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ var (
1414
)
1515

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

@@ -30,7 +30,7 @@ func ParseFixedPointDecimal(s string, p uint8) (int64, error) {
3030
var pn = int8(p)
3131
var d int8 = -1 // current decimal position
3232
var n int64 // output
33-
for _, ch := range []byte(s) {
33+
for _, ch := range s {
3434
if d == pn {
3535
break
3636
}

parser_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func FuzzParseFixedPointDecimal(f *testing.F) {
2929
f.Add("-" + tc)
3030
}
3131
f.Fuzz(func(t *testing.T, s string) {
32-
v, err := fpdecimal.ParseFixedPointDecimal(s, 3)
32+
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), 3)
3333
if err != nil {
3434
if v != 0 {
3535
t.Errorf("has to be 0 on error")

printer.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
const zeroPrefix = "0.000000000000000000000000000000000000"
88

99
// FixedPointDecimalToString formats fixed-point decimal to string
10-
func FixedPointDecimalToString(v int64, p int) string {
10+
func FixedPointDecimalToString(v int64, p uint8) string {
1111
// max int64: +9223372036854775.807
1212
// min int64: -9223372036854775.808
1313
// max bytes int64: 21
@@ -19,7 +19,7 @@ func FixedPointDecimalToString(v int64, p int) string {
1919
// AppendFixedPointDecimal appends formatted fixed point decimal to destination buffer.
2020
// Returns appended slice.
2121
// This is efficient for avoiding memory copy.
22-
func AppendFixedPointDecimal(b []byte, v int64, p int) []byte {
22+
func AppendFixedPointDecimal(b []byte, v int64, p uint8) []byte {
2323
if v == 0 {
2424
return append(b, '0')
2525
}
@@ -35,20 +35,20 @@ func AppendFixedPointDecimal(b []byte, v int64, p int) []byte {
3535

3636
// strconv.AppendInt is very efficient.
3737
// Efficient converting int64 to ASCII is not as trivial.
38-
s := len(b)
38+
s := uint8(len(b))
3939
b = strconv.AppendInt(b, v, 10)
4040

4141
// has whole?
42-
if len(b)-s > p {
42+
if uint8(len(b))-s > p {
4343
// place decimal point
44-
i := len(b) - p
44+
i := uint8(len(b)) - p
4545
b = append(b, 0)
4646
copy(b[i+1:], b[i:])
4747
b[i] = '.'
4848
} else {
4949
// append zeroes and decimal point
50-
i := 2 + p - (len(b) - s)
51-
for j := 0; j < i; j++ {
50+
i := 2 + p - (uint8(len(b)) - s)
51+
for j := uint8(0); j < i; j++ {
5252
b = append(b, 0)
5353
}
5454
copy(b[s+i:], b[s:])

printer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func FuzzFixedPointDecimalToString(f *testing.F) {
4141
s := fmt.Sprintf("%.3f", r)
4242
rs, _ := strconv.ParseFloat(s, 64)
4343

44-
v, err := fpdecimal.ParseFixedPointDecimal(s, 3)
44+
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), 3)
4545
if err != nil {
4646
t.Errorf(err.Error())
4747
}

0 commit comments

Comments
 (0)