Skip to content

Commit bbd52ee

Browse files
committed
msgpack: add string() for decimal
Added a benchmark, which shows that the code is optimized two or more times for string conversion than the code from the library. Added #322
1 parent 6cc168b commit bbd52ee

File tree

2 files changed

+353
-13
lines changed

2 files changed

+353
-13
lines changed

decimal/decimal_bench_test.go

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
package decimal
2+
3+
import (
4+
"math/rand"
5+
"strconv"
6+
"strings"
7+
"testing"
8+
"time"
9+
)
10+
11+
// Minimal benchmark without dependencies.
12+
func BenchmarkMinimal(b *testing.B) {
13+
dec := MustMakeDecimal("123.45")
14+
15+
b.Run("StringOptimized", func(b *testing.B) {
16+
for i := 0; i < b.N; i++ {
17+
_ = dec.StringOptimized()
18+
}
19+
})
20+
21+
b.Run("StandardString", func(b *testing.B) {
22+
for i := 0; i < b.N; i++ {
23+
_ = dec.Decimal.String()
24+
}
25+
})
26+
}
27+
28+
// Benchmark for small numbers (optimized conversion path).
29+
func BenchmarkDecimalString_SmallNumbers(b *testing.B) {
30+
smallNumbers := []string{
31+
"123.45",
32+
"-123.45",
33+
"0.00123",
34+
"100.00",
35+
"999.99",
36+
"42",
37+
"-42",
38+
"0.000001",
39+
"1234567.89",
40+
"-987654.32",
41+
}
42+
43+
decimals := make([]Decimal, len(smallNumbers))
44+
for i, str := range smallNumbers {
45+
decimals[i] = MustMakeDecimal(str)
46+
}
47+
48+
b.ResetTimer()
49+
b.Run("StringOptimized", func(b *testing.B) {
50+
for i := 0; i < b.N; i++ {
51+
for _, dec := range decimals {
52+
_ = dec.StringOptimized()
53+
}
54+
}
55+
})
56+
57+
b.Run("StandardString", func(b *testing.B) {
58+
for i := 0; i < b.N; i++ {
59+
for _, dec := range decimals {
60+
_ = dec.Decimal.String()
61+
}
62+
}
63+
})
64+
}
65+
66+
// A benchmark for the boundary cases of int64.
67+
func BenchmarkDecimalString_Int64Boundaries(b *testing.B) {
68+
boundaryNumbers := []string{
69+
"9223372036854775807", // max int64
70+
"-9223372036854775808", // min int64
71+
"9223372036854775806",
72+
"-9223372036854775807",
73+
}
74+
75+
decimals := make([]Decimal, len(boundaryNumbers))
76+
for i, str := range boundaryNumbers {
77+
decimals[i] = MustMakeDecimal(str)
78+
}
79+
80+
b.ResetTimer()
81+
b.Run("StringOptimized", func(b *testing.B) {
82+
for i := 0; i < b.N; i++ {
83+
for _, dec := range decimals {
84+
_ = dec.StringOptimized()
85+
}
86+
}
87+
})
88+
89+
b.Run("StandardString", func(b *testing.B) {
90+
for i := 0; i < b.N; i++ {
91+
for _, dec := range decimals {
92+
_ = dec.Decimal.String()
93+
}
94+
}
95+
})
96+
}
97+
98+
// Benchmark for large numbers (fallback path).
99+
func BenchmarkDecimalString_LargeNumbers(b *testing.B) {
100+
largeNumbers := []string{
101+
"123456789012345678901234567890.123456789",
102+
"-123456789012345678901234567890.123456789",
103+
"99999999999999999999999999999999999999",
104+
"-99999999999999999999999999999999999999",
105+
}
106+
107+
decimals := make([]Decimal, len(largeNumbers))
108+
for i, str := range largeNumbers {
109+
decimals[i] = MustMakeDecimal(str)
110+
}
111+
112+
b.ResetTimer()
113+
b.Run("StringOptimized", func(b *testing.B) {
114+
for i := 0; i < b.N; i++ {
115+
for _, dec := range decimals {
116+
_ = dec.StringOptimized()
117+
}
118+
}
119+
})
120+
121+
b.Run("StandardString", func(b *testing.B) {
122+
for i := 0; i < b.N; i++ {
123+
for _, dec := range decimals {
124+
_ = dec.Decimal.String()
125+
}
126+
}
127+
})
128+
}
129+
130+
// A benchmark for mixed numbers (real-world scenarios).
131+
func BenchmarkDecimalString_Mixed(b *testing.B) {
132+
mixedNumbers := []string{
133+
"0",
134+
"1",
135+
"-1",
136+
"0.5",
137+
"-0.5",
138+
"123.456",
139+
"1000000.000001",
140+
"9223372036854775807",
141+
"123456789012345678901234567890.123456789",
142+
}
143+
144+
decimals := make([]Decimal, len(mixedNumbers))
145+
for i, str := range mixedNumbers {
146+
decimals[i] = MustMakeDecimal(str)
147+
}
148+
149+
b.ResetTimer()
150+
b.Run("StringOptimized", func(b *testing.B) {
151+
for i := 0; i < b.N; i++ {
152+
for _, dec := range decimals {
153+
_ = dec.StringOptimized()
154+
}
155+
}
156+
})
157+
158+
b.Run("StandardString", func(b *testing.B) {
159+
for i := 0; i < b.N; i++ {
160+
for _, dec := range decimals {
161+
_ = dec.Decimal.String()
162+
}
163+
}
164+
})
165+
}
166+
167+
// A benchmark for numbers with different precision.
168+
func BenchmarkDecimalString_DifferentPrecision(b *testing.B) {
169+
testCases := []struct {
170+
name string
171+
value string
172+
}{
173+
{"Integer", "1234567890"},
174+
{"SmallDecimal", "0.000000001"},
175+
{"MediumDecimal", "123.456789"},
176+
{"LargeDecimal", "1234567890.123456789"},
177+
}
178+
179+
for _, tc := range testCases {
180+
b.Run(tc.name, func(b *testing.B) {
181+
dec := MustMakeDecimal(tc.value)
182+
183+
b.Run("StringOptimized", func(b *testing.B) {
184+
for i := 0; i < b.N; i++ {
185+
_ = dec.StringOptimized()
186+
}
187+
})
188+
189+
b.Run("StandardString", func(b *testing.B) {
190+
for i := 0; i < b.N; i++ {
191+
_ = dec.Decimal.String()
192+
}
193+
})
194+
})
195+
}
196+
}
197+
198+
// A benchmark with random numbers for statistical significance.
199+
func BenchmarkDecimalString_Random(b *testing.B) {
200+
rand.Seed(time.Now().UnixNano())
201+
202+
// Generating random numbers in the int64 range.
203+
generateRandomDecimal := func() Decimal {
204+
// 70% chance for small numbers, 30% for large numbers.
205+
if rand.Float64() < 0.7 {
206+
// Числа в диапазоне int64
207+
value := rand.Int63n(1000000000) - 500000000
208+
scale := rand.Intn(10)
209+
210+
if scale == 0 {
211+
return MustMakeDecimal(strconv.FormatInt(value, 10))
212+
}
213+
214+
// For numbers with a fractional part.
215+
str := strconv.FormatInt(value, 10)
216+
if value < 0 {
217+
str = str[1:] // убираем минус
218+
}
219+
220+
if len(str) > scale {
221+
integerPart := str[:len(str)-scale]
222+
fractionalPart := str[len(str)-scale:]
223+
result := integerPart + "." + fractionalPart
224+
if value < 0 {
225+
result = "-" + result
226+
}
227+
return MustMakeDecimal(result)
228+
} else {
229+
zeros := scale - len(str)
230+
result := "0." + strings.Repeat("0", zeros) + str
231+
if value < 0 {
232+
result = "-" + result
233+
}
234+
return MustMakeDecimal(result)
235+
}
236+
} else {
237+
// Large numbers (fallback) - we generate correct strings.
238+
// Generating a 30-digit number.
239+
bigDigits := make([]byte, 30)
240+
for i := range bigDigits {
241+
bigDigits[i] = byte(rand.Intn(10) + '0')
242+
}
243+
// Убираем ведущие нули
244+
for len(bigDigits) > 1 && bigDigits[0] == '0' {
245+
bigDigits = bigDigits[1:]
246+
}
247+
248+
bigNum := string(bigDigits)
249+
scale := rand.Intn(10)
250+
251+
if scale == 0 {
252+
if rand.Float64() < 0.5 {
253+
bigNum = "-" + bigNum
254+
}
255+
return MustMakeDecimal(bigNum)
256+
}
257+
258+
if scale < len(bigNum) {
259+
integerPart := bigNum[:len(bigNum)-scale]
260+
fractionalPart := bigNum[len(bigNum)-scale:]
261+
result := integerPart + "." + fractionalPart
262+
if rand.Float64() < 0.5 {
263+
result = "-" + result
264+
}
265+
return MustMakeDecimal(result)
266+
} else {
267+
zeros := scale - len(bigNum)
268+
result := "0." + strings.Repeat("0", zeros) + bigNum
269+
if rand.Float64() < 0.5 {
270+
result = "-" + result
271+
}
272+
return MustMakeDecimal(result)
273+
}
274+
}
275+
}
276+
277+
b.ResetTimer()
278+
b.Run("StringOptimized", func(b *testing.B) {
279+
total := 0
280+
for i := 0; i < b.N; i++ {
281+
dec := generateRandomDecimal()
282+
result := dec.StringOptimized()
283+
total += len(result)
284+
}
285+
_ = total
286+
})
287+
288+
b.Run("StandardString", func(b *testing.B) {
289+
total := 0
290+
for i := 0; i < b.N; i++ {
291+
dec := generateRandomDecimal()
292+
result := dec.Decimal.String()
293+
total += len(result)
294+
}
295+
_ = total
296+
})
297+
}
298+
299+
// A benchmark for checking memory allocations.
300+
func BenchmarkDecimalString_MemoryAllocations(b *testing.B) {
301+
testNumbers := []string{
302+
"123.45",
303+
"0.001",
304+
"9223372036854775807",
305+
"123456789012345678901234567890.123456789",
306+
}
307+
308+
decimals := make([]Decimal, len(testNumbers))
309+
for i, str := range testNumbers {
310+
decimals[i] = MustMakeDecimal(str)
311+
}
312+
313+
b.ResetTimer()
314+
315+
b.Run("StringOptimized", func(b *testing.B) {
316+
for i := 0; i < b.N; i++ {
317+
for _, dec := range decimals {
318+
_ = dec.StringOptimized()
319+
}
320+
}
321+
})
322+
323+
b.Run("StandardString", func(b *testing.B) {
324+
for i := 0; i < b.N; i++ {
325+
for _, dec := range decimals {
326+
_ = dec.Decimal.String()
327+
}
328+
}
329+
})
330+
}

decimal/decimal_test.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -867,19 +867,29 @@ func Test100_00(t *testing.T) {
867867
t.Logf("StringOptimized: %q", result)
868868
}
869869

870-
// Minimal benchmark without dependencies.
871-
func BenchmarkMinimal(b *testing.B) {
872-
dec := MustMakeDecimal("123.45")
870+
func TestLargeNumberStringOptimized(t *testing.T) {
871+
largeNumber := "123456789012345678901234567890.123456789"
872+
dec, err := MakeDecimalFromString(largeNumber)
873+
if err != nil {
874+
t.Fatalf("Failed to create decimal: %v", err)
875+
}
873876

874-
b.Run("StringOptimized", func(b *testing.B) {
875-
for i := 0; i < b.N; i++ {
876-
_ = dec.StringOptimized()
877-
}
878-
})
877+
// Check that the coefficient does not fit in int64.
878+
coefficient := dec.Decimal.Coefficient()
879+
if coefficient.IsInt64() {
880+
t.Error("Expected coefficient to be too large for int64")
881+
}
879882

880-
b.Run("StandardString", func(b *testing.B) {
881-
for i := 0; i < b.N; i++ {
882-
_ = dec.Decimal.String()
883-
}
884-
})
883+
optimized := dec.StringOptimized()
884+
standard := dec.Decimal.String()
885+
886+
if optimized != standard {
887+
t.Errorf("Results differ: optimized=%s, standard=%s", optimized, standard)
888+
}
889+
890+
if optimized != largeNumber {
891+
t.Errorf("Expected %s, got %s", largeNumber, optimized)
892+
}
893+
894+
t.Logf("Large number handled via fallback: %s", optimized)
885895
}

0 commit comments

Comments
 (0)