From 0222aeba7f36aa6708e7ce0a8767d5f60923045c Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 14 May 2026 14:38:24 -0500 Subject: [PATCH] perf: eliminate allocations in scanner and formatter --- big.go | 11 +++++++---- bytes_scanner.go | 41 +++++++++++++++++++++++++++++++++++++++++ format.go | 18 +++++++++++++++--- scan.go | 14 +++++++------- 4 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 bytes_scanner.go diff --git a/big.go b/big.go index ff4d9cd..38c50c4 100644 --- a/big.go +++ b/big.go @@ -1406,7 +1406,8 @@ func (x *Big) Scale() int { // Scan implements fmt.Scanner. func (z *Big) Scan(state fmt.ScanState, verb rune) error { - return z.scan(byteReader{state}) + scanner := byteScanner{r: byteReader{state}} + return z.scan(&scanner) } // Set sets z to x and returns z. @@ -1665,7 +1666,8 @@ var Regexp = regexp.MustCompile(`(?i)(([+-]?(\d+\.\d*|\.?\d+)([eE][+-]?\d+)?)|(i // SetString will only return (nil, false) if a library error // occurs. In general, it safe to ignore the bool result. func (z *Big) SetString(s string) (*Big, bool) { - if err := z.scan(strings.NewReader(s)); err != nil { + scanner := byteScanner{s: s} + if err := z.scan(&scanner); err != nil { return nil, false } return z, true @@ -1766,7 +1768,8 @@ func (z *Big) UnmarshalJSON(data []byte) error { // UnmarshalText implements encoding.TextUnmarshaler. func (z *Big) UnmarshalText(data []byte) error { - return z.scan(bytes.NewReader(data)) + scanner := byteScanner{b: data} + return z.scan(&scanner) } // validate ensures x's internal state is correct. There's no need for it to @@ -1815,4 +1818,4 @@ func (x *Big) validate() { default: panic(fmt.Sprintf("invalid form %s", x.form)) } -} +} \ No newline at end of file diff --git a/bytes_scanner.go b/bytes_scanner.go new file mode 100644 index 0000000..1c1ee3c --- /dev/null +++ b/bytes_scanner.go @@ -0,0 +1,41 @@ +package decimal + +import "io" + +type byteScanner struct { + r io.ByteScanner + s string + b []byte + i int +} + +func (s *byteScanner) ReadByte() (byte, error) { + if s.r != nil { + return s.r.ReadByte() + } + if s.b != nil { + if s.i >= len(s.b) { + return 0, io.EOF + } + c := s.b[s.i] + s.i++ + return c, nil + } + if s.i >= len(s.s) { + return 0, io.EOF + } + c := s.s[s.i] + s.i++ + return c, nil +} + +func (s *byteScanner) UnreadByte() error { + if s.r != nil { + return s.r.UnreadByte() + } + if s.i <= 0 { + return io.EOF + } + s.i-- + return nil +} \ No newline at end of file diff --git a/format.go b/format.go index 8a34100..a47f41e 100644 --- a/format.go +++ b/format.go @@ -154,6 +154,18 @@ func (f *formatter) Write(p []byte) (n int, err error) { var sciE = [2]byte{GDA: 'E', Go: 'e'} +func (f *formatter) writeZeros(n int) { + const zeros = "0000000000000000000000000000000000000000000000000000000000000000" // 64 + for n > 0 { + chunk := n + if chunk > len(zeros) { + chunk = len(zeros) + } + f.WriteString(zeros[:chunk]) + n -= chunk + } +} + func (f *formatter) format(x *Big, format format, e byte) { if x == nil { f.WriteString("") @@ -186,7 +198,7 @@ func (f *formatter) format(x *Big, format format, e byte) { f.WriteByte('0') } else { f.WriteString("0.") - io.CopyN(f, zeroReader{}, int64(f.width)) + f.writeZeros(f.width) } return } @@ -237,7 +249,7 @@ func (f *formatter) format(x *Big, format format, e byte) { // No decimal places, write b and fill with zeros. if format == plain && exp > 0 { f.Write(b) - io.CopyN(f, zeroReader{}, int64(exp)) + f.writeZeros(exp) return } } @@ -282,7 +294,7 @@ func (f *formatter) formatPlain(b []byte, exp int) { // log10(b) < scale, so before p "0s" and before b: 0.00000123456 default: f.WriteString(zeroRadix) - io.CopyN(f, zeroReader{}, -int64(radix)) + f.writeZeros(-radix) end := len(b) if f.prec < end { diff --git a/scan.go b/scan.go index 69f9a25..4e404d3 100644 --- a/scan.go +++ b/scan.go @@ -9,7 +9,7 @@ import ( "github.com/ericlagergren/decimal/internal/c" ) -func (z *Big) scan(r io.ByteScanner) error { +func (z *Big) scan(r *byteScanner) error { if debug { defer func() { z.validate() }() } @@ -97,7 +97,7 @@ func (z *Big) scan(r io.ByteScanner) error { return nil } -func scanSign(r io.ByteScanner) (bool, error) { +func scanSign(r *byteScanner) (bool, error) { ch, err := r.ReadByte() if err != nil { return false, err @@ -112,7 +112,7 @@ func scanSign(r io.ByteScanner) (bool, error) { } } -func (z *Big) scanForm(r io.ByteScanner) (form, error) { +func (z *Big) scanForm(r *byteScanner) (form, error) { ch, err := r.ReadByte() if err != nil { return 0, err @@ -186,7 +186,7 @@ func (z *Big) scanForm(r io.ByteScanner) (form, error) { return qnan, nil } -func (z *Big) scanInfinity(r io.ByteScanner) (form, error) { +func (z *Big) scanInfinity(r *byteScanner) (form, error) { const ( s = "infinity" ) @@ -208,7 +208,7 @@ func (z *Big) scanInfinity(r io.ByteScanner) (form, error) { return inf, nil } -func (z *Big) scanMant(r io.ByteScanner) (err error) { +func (z *Big) scanMant(r *byteScanner) (err error) { buf := make([]byte, 0, 20) seenDot := false dot := 0 @@ -282,7 +282,7 @@ Loop: return nil } -func (z *Big) scanExponent(r io.ByteScanner) error { +func (z *Big) scanExponent(r *byteScanner) error { ch, err := r.ReadByte() if err != nil { if err == io.EOF { @@ -375,4 +375,4 @@ func (r byteReader) ReadByte() (byte, error) { func (r byteReader) UnreadByte() error { return r.UnreadRune() -} +} \ No newline at end of file