math/big: faster string conversion routines
Eliminated unnecessary string conversions throughout and removed
(internal) capability for arbitrary character sets in conversion
routines (functionality was not exported and not used internally).
benchmark old ns/op new ns/op delta
BenchmarkDecimalConversion-8 198283 187085 -5.65%
BenchmarkStringPiParallel-8 46116 47822 +3.70%
BenchmarkString10Base2-8 216 166 -23.15%
BenchmarkString100Base2-8 886 762 -14.00%
BenchmarkString1000Base2-8 7296 6625 -9.20%
BenchmarkString10000Base2-8 72371 65563 -9.41%
BenchmarkString100000Base2-8 725849 672766 -7.31%
BenchmarkString10Base8-8 160 114 -28.75%
BenchmarkString100Base8-8 398 309 -22.36%
BenchmarkString1000Base8-8 2650 2244 -15.32%
BenchmarkString10000Base8-8 24974 21745 -12.93%
BenchmarkString100000Base8-8 245457 217489 -11.39%
BenchmarkString10Base10-8 337 288 -14.54%
BenchmarkString100Base10-8 1298 1046 -19.41%
BenchmarkString1000Base10-8 6200 5752 -7.23%
BenchmarkString10000Base10-8 24942 22589 -9.43%
BenchmarkString100000Base10-8 8012921 7947152 -0.82%
BenchmarkString10Base16-8 156 107 -31.41%
BenchmarkString100Base16-8 344 255 -25.87%
BenchmarkString1000Base16-8 2067 1705 -17.51%
BenchmarkString10000Base16-8 19026 16112 -15.32%
BenchmarkString100000Base16-8 184038 163457 -11.18%
Change-Id: I68bd807529bd9b985f4b6ac2a87764bcc1a7d2f7
Reviewed-on: https://go-review.googlesource.com/14926
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/src/math/big/decimal.go b/src/math/big/decimal.go
index b9e181d..28cddf9 100644
--- a/src/math/big/decimal.go
+++ b/src/math/big/decimal.go
@@ -80,7 +80,7 @@
}
// Convert mantissa into decimal representation.
- s := m.decimalString() // TODO(gri) avoid string conversion here
+ s := m.itoa(10)
n := len(s)
x.exp = n
// Trim trailing zeros; instead the exponent is tracking
diff --git a/src/math/big/ftoa.go b/src/math/big/ftoa.go
index 506a6cb9..3727b7d 100644
--- a/src/math/big/ftoa.go
+++ b/src/math/big/ftoa.go
@@ -9,9 +9,9 @@
package big
import (
+ "bytes"
"fmt"
"strconv"
- "strings"
)
// Text converts the floating-point number x to a string according
@@ -323,7 +323,7 @@
m = nat(nil).shr(m, uint(w-x.prec))
}
- buf = append(buf, m.decimalString()...)
+ buf = append(buf, m.itoa(10)...)
buf = append(buf, 'p')
e := int64(x.exp) - int64(x.prec)
if e >= 0 {
@@ -357,7 +357,7 @@
m = m[i:]
buf = append(buf, "0x."...)
- buf = append(buf, strings.TrimRight(m.hexString(), "0")...)
+ buf = append(buf, bytes.TrimRight(m.itoa(16), "0")...)
buf = append(buf, 'p')
if x.exp >= 0 {
buf = append(buf, '+')
diff --git a/src/math/big/intconv.go b/src/math/big/intconv.go
index 737d176..4693b17 100644
--- a/src/math/big/intconv.go
+++ b/src/math/big/intconv.go
@@ -17,25 +17,9 @@
case x == nil:
return "<nil>"
case x.neg:
- return "-" + x.abs.decimalString()
+ return "-" + string(x.abs.itoa(10))
}
- return x.abs.decimalString()
-}
-
-func charset(ch rune) string {
- switch ch {
- case 'b':
- return lowercaseDigits[0:2]
- case 'o':
- return lowercaseDigits[0:8]
- case 'd', 's', 'v':
- return lowercaseDigits[0:10]
- case 'x':
- return lowercaseDigits[0:16]
- case 'X':
- return uppercaseDigits[0:16]
- }
- return "" // unknown format
+ return string(x.abs.itoa(10))
}
// write count copies of text to s
@@ -60,15 +44,24 @@
// right justification.
//
func (x *Int) Format(s fmt.State, ch rune) {
- cs := charset(ch)
-
- // special cases
- switch {
- case cs == "":
+ // determine base
+ var base int
+ switch ch {
+ case 'b':
+ base = 2
+ case 'o':
+ base = 8
+ case 'd', 's', 'v':
+ base = 10
+ case 'x', 'X':
+ base = 16
+ default:
// unknown format
fmt.Fprintf(s, "%%!%c(big.Int=%s)", ch, x.String())
return
- case x == nil:
+ }
+
+ if x == nil {
fmt.Fprint(s, "<nil>")
return
}
@@ -97,8 +90,15 @@
}
}
- // determine digits with base set by len(cs) and digit characters from cs
- digits := x.abs.string(cs)
+ digits := x.abs.itoa(base)
+ if ch == 'X' {
+ // faster than bytes.ToUpper
+ for i, d := range digits {
+ if 'a' <= d && d <= 'z' {
+ digits[i] = 'A' + (d - 'a')
+ }
+ }
+ }
// number of characters for the three classes of number padding
var left int // space characters to left of digits for right justification ("%8d")
@@ -111,7 +111,7 @@
switch {
case len(digits) < precision:
zeros = precision - len(digits) // count of zero padding
- case digits == "0" && precision == 0:
+ case len(digits) == 1 && digits[0] == '0' && precision == 0:
return // print nothing if zero value (x == 0) and zero precision ("." or ".0")
}
}
@@ -137,7 +137,7 @@
writeMultiple(s, sign, 1)
writeMultiple(s, prefix, 1)
writeMultiple(s, "0", zeros)
- writeMultiple(s, digits, 1)
+ s.Write(digits)
writeMultiple(s, " ", right)
}
diff --git a/src/math/big/nat_test.go b/src/math/big/nat_test.go
index 7ac3cb8..3def120 100644
--- a/src/math/big/nat_test.go
+++ b/src/math/big/nat_test.go
@@ -158,7 +158,7 @@
func TestMulRangeN(t *testing.T) {
for i, r := range mulRangesN {
- prod := nat(nil).mulRange(r.a, r.b).decimalString()
+ prod := string(nat(nil).mulRange(r.a, r.b).itoa(10))
if prod != r.prod {
t.Errorf("#%d: got %s; want %s", i, prod, r.prod)
}
@@ -326,7 +326,7 @@
for i := uint(0); i <= 3*_W; i++ {
n := y.trailingZeroBits()
if n != i {
- t.Errorf("got 0x%s.trailingZeroBits() = %d; want %d", y.hexString(), n, i)
+ t.Errorf("got 0x%s.trailingZeroBits() = %d; want %d", y.itoa(16), n, i)
}
y = y.shl(y, 1)
}
@@ -388,7 +388,7 @@
z := nat(nil).montgomery(x, y, m, k0, len(m))
z = z.norm()
if z.cmp(out) != 0 {
- t.Errorf("#%d got %s want %s", i, z.decimalString(), out.decimalString())
+ t.Errorf("#%d got %s want %s", i, z.itoa(10), out.itoa(10))
}
}
}
@@ -429,7 +429,7 @@
z := nat(nil).expNN(x, y, m)
if z.cmp(out) != 0 {
- t.Errorf("#%d got %s want %s", i, z.decimalString(), out.decimalString())
+ t.Errorf("#%d got %s want %s", i, z.itoa(10), out.itoa(10))
}
}
}
@@ -486,7 +486,7 @@
func TestFibo(t *testing.T) {
for i, want := range fiboNums {
n := i * 10
- got := fibo(n).decimalString()
+ got := string(fibo(n).itoa(10))
if got != want {
t.Errorf("fibo(%d) failed: got %s want %s", n, got, want)
}
diff --git a/src/math/big/natconv.go b/src/math/big/natconv.go
index 80da307..96c0fad 100644
--- a/src/math/big/natconv.go
+++ b/src/math/big/natconv.go
@@ -14,6 +14,11 @@
"sync"
)
+const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
+
+// Note: MaxBase = len(digits), but it must remain a rune constant
+// for API compatibility.
+
// MaxBase is the largest number base accepted for string conversions.
const MaxBase = 'z' - 'a' + 10 + 1
@@ -229,45 +234,25 @@
return
}
-// Character sets for string conversion.
-const (
- lowercaseDigits = "0123456789abcdefghijklmnopqrstuvwxyz"
- uppercaseDigits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-)
-
-// decimalString returns a decimal representation of x.
-// It calls x.string with the charset "0123456789".
-func (x nat) decimalString() string {
- return x.string(lowercaseDigits[:10])
-}
-
-// hexString returns a hexadecimal representation of x.
-// It calls x.string with the charset "0123456789abcdef".
-func (x nat) hexString() string {
- return x.string(lowercaseDigits[:16])
-}
-
-// string converts x to a string using digits from a charset; a digit with
-// value d is represented by charset[d]. The conversion base is determined
-// by len(charset), which must be >= 2 and <= 256.
-func (x nat) string(charset string) string {
- b := Word(len(charset))
- if b < 2 || b > 256 {
- panic("invalid character set length")
+// itoa converts x to an ASCII representation in the given base;
+// base must be between 2 and MaxBase, inclusive.
+func (x nat) itoa(base int) []byte {
+ if base < 2 || base > MaxBase {
+ panic("invalid base")
}
// x == 0
if len(x) == 0 {
- return string(charset[0])
+ return []byte("0")
}
// len(x) > 0
// allocate buffer for conversion
- i := int(float64(x.bitLen())/math.Log2(float64(b))) + 1 // off by one at most
+ i := int(float64(x.bitLen())/math.Log2(float64(base))) + 1 // off by 1 at most
s := make([]byte, i)
// convert power of two and non power of two bases separately
- if b == b&-b {
+ if b := Word(base); b == b&-b {
// shift is base b digit size in bits
shift := trailingZeroBits(b) // shift > 0 because b >= 2
mask := Word(1<<shift - 1)
@@ -279,7 +264,7 @@
// convert full digits
for nbits >= shift {
i--
- s[i] = charset[w&mask]
+ s[i] = digits[w&mask]
w >>= shift
nbits -= shift
}
@@ -293,7 +278,7 @@
// partial digit in current word w (== x[k-1]) and next word x[k]
w |= x[k] << nbits
i--
- s[i] = charset[w&mask]
+ s[i] = digits[w&mask]
// advance
w = x[k] >> (shift - nbits)
@@ -304,7 +289,7 @@
// convert digits of most-significant word w (omit leading zeros)
for w != 0 {
i--
- s[i] = charset[w&mask]
+ s[i] = digits[w&mask]
w >>= shift
}
@@ -319,18 +304,18 @@
q := nat(nil).set(x)
// convert q to string s in base b
- q.convertWords(s, charset, b, ndigits, bb, table)
+ q.convertWords(s, b, ndigits, bb, table)
// strip leading zeros
// (x != 0; thus s must contain at least one non-zero digit
// and the loop will terminate)
i = 0
- for zero := charset[0]; s[i] == zero; {
+ for s[i] == '0' {
i++
}
}
- return string(s[i:])
+ return s[i:]
}
// Convert words of q to base b digits in s. If q is large, it is recursively "split in half"
@@ -349,7 +334,7 @@
// ~30x for 20000 digits. Use nat_test.go's BenchmarkLeafSize tests to optimize leafSize for
// specific hardware.
//
-func (q nat) convertWords(s []byte, charset string, b Word, ndigits int, bb Word, table []divisor) {
+func (q nat) convertWords(s []byte, b Word, ndigits int, bb Word, table []divisor) {
// split larger blocks recursively
if table != nil {
// len(q) > leafSize > 0
@@ -374,8 +359,8 @@
// convert subblocks and collect results in s[:h] and s[h:]
h := len(s) - table[index].ndigits
- r.convertWords(s[h:], charset, b, ndigits, bb, table[0:index])
- s = s[:h] // == q.convertWords(s, charset, b, ndigits, bb, table[0:index+1])
+ r.convertWords(s[h:], b, ndigits, bb, table[0:index])
+ s = s[:h] // == q.convertWords(s, b, ndigits, bb, table[0:index+1])
}
}
@@ -393,7 +378,7 @@
// this appears to be faster for BenchmarkString10000Base10
// and smaller strings (but a bit slower for larger ones)
t := r / 10
- s[i] = charset[r-t<<3-t-t] // TODO(gri) replace w/ t*10 once compiler produces better code
+ s[i] = '0' + byte(r-t<<3-t-t) // TODO(gri) replace w/ t*10 once compiler produces better code
r = t
}
}
@@ -403,17 +388,16 @@
q, r = q.divW(q, bb)
for j := 0; j < ndigits && i > 0; j++ {
i--
- s[i] = charset[r%b]
+ s[i] = digits[r%b]
r /= b
}
}
}
// prepend high-order zeros
- zero := charset[0]
for i > 0 { // while need more leading zeros
i--
- s[i] = zero
+ s[i] = '0'
}
}
diff --git a/src/math/big/natconv_test.go b/src/math/big/natconv_test.go
index f321fbc..687b706 100644
--- a/src/math/big/natconv_test.go
+++ b/src/math/big/natconv_test.go
@@ -5,20 +5,19 @@
package big
import (
+ "bytes"
"io"
"strings"
"testing"
)
-func toString(x nat, charset string) string {
- base := len(charset)
-
+func itoa(x nat, base int) []byte {
// special cases
switch {
case base < 2:
panic("illegal base")
case len(x) == 0:
- return string(charset[0])
+ return []byte("0")
}
// allocate buffer for conversion
@@ -33,54 +32,53 @@
i--
var r Word
q, r = q.divW(q, Word(base))
- s[i] = charset[r]
+ s[i] = digits[r]
}
- return string(s[i:])
+ return s[i:]
}
var strTests = []struct {
x nat // nat value to be converted
- c string // conversion charset
+ b int // conversion base
s string // expected result
}{
- {nil, "01", "0"},
- {nat{1}, "01", "1"},
- {nat{0xc5}, "01", "11000101"},
- {nat{03271}, lowercaseDigits[:8], "3271"},
- {nat{10}, lowercaseDigits[:10], "10"},
- {nat{1234567890}, uppercaseDigits[:10], "1234567890"},
- {nat{0xdeadbeef}, lowercaseDigits[:16], "deadbeef"},
- {nat{0xdeadbeef}, uppercaseDigits[:16], "DEADBEEF"},
- {nat{0x229be7}, lowercaseDigits[:17], "1a2b3c"},
- {nat{0x309663e6}, uppercaseDigits[:32], "O9COV6"},
+ {nil, 2, "0"},
+ {nat{1}, 2, "1"},
+ {nat{0xc5}, 2, "11000101"},
+ {nat{03271}, 8, "3271"},
+ {nat{10}, 10, "10"},
+ {nat{1234567890}, 10, "1234567890"},
+ {nat{0xdeadbeef}, 16, "deadbeef"},
+ {nat{0x229be7}, 17, "1a2b3c"},
+ {nat{0x309663e6}, 32, "o9cov6"},
}
func TestString(t *testing.T) {
- // test invalid character set explicitly
+ // test invalid base explicitly
var panicStr string
func() {
defer func() {
panicStr = recover().(string)
}()
- natOne.string("0")
+ natOne.itoa(1)
}()
- if panicStr != "invalid character set length" {
- t.Errorf("expected panic for invalid character set")
+ if panicStr != "invalid base" {
+ t.Errorf("expected panic for invalid base")
}
for _, a := range strTests {
- s := a.x.string(a.c)
+ s := string(a.x.itoa(a.b))
if s != a.s {
t.Errorf("string%+v\n\tgot s = %s; want %s", a, s, a.s)
}
- x, b, _, err := nat(nil).scan(strings.NewReader(a.s), len(a.c), false)
+ x, b, _, err := nat(nil).scan(strings.NewReader(a.s), a.b, false)
if x.cmp(a.x) != 0 {
t.Errorf("scan%+v\n\tgot z = %v; want %v", a, x, a.x)
}
- if b != len(a.c) {
- t.Errorf("scan%+v\n\tgot b = %d; want %d", a, b, len(a.c))
+ if b != a.b {
+ t.Errorf("scan%+v\n\tgot b = %d; want %d", a, b, a.b)
}
if err != nil {
t.Errorf("scan%+v\n\tgot error = %s", a, err)
@@ -236,7 +234,7 @@
if err != nil {
t.Errorf("scanning pi: %s", err)
}
- if s := z.decimalString(); s != pi {
+ if s := string(z.itoa(10)); s != pi {
t.Errorf("scanning pi: got %s", s)
}
}
@@ -265,12 +263,12 @@
func BenchmarkStringPiParallel(b *testing.B) {
var x nat
x, _, _, _ = x.scan(strings.NewReader(pi), 0, false)
- if x.decimalString() != pi {
+ if string(x.itoa(10)) != pi {
panic("benchmark incorrect: conversion failed")
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
- x.decimalString()
+ x.itoa(10)
}
})
}
@@ -304,15 +302,14 @@
var z nat
z = z.expWW(x, y)
- var s string
- s = z.string(lowercaseDigits[:base])
- if t := toString(z, lowercaseDigits[:base]); t != s {
+ s := z.itoa(base)
+ if t := itoa(z, base); !bytes.Equal(s, t) {
b.Fatalf("scanning: got %s; want %s", s, t)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
- z.scan(strings.NewReader(s), base, false)
+ z.scan(bytes.NewReader(s), base, false)
}
}
@@ -344,11 +341,11 @@
b.StopTimer()
var z nat
z = z.expWW(x, y)
- z.string(lowercaseDigits[:base]) // warm divisor cache
+ z.itoa(base) // warm divisor cache
b.StartTimer()
for i := 0; i < b.N; i++ {
- _ = z.string(lowercaseDigits[:base])
+ _ = z.itoa(base)
}
}
@@ -372,7 +369,7 @@
func BenchmarkLeafSize32(b *testing.B) { LeafSizeHelper(b, 10, 32) } // try some large lengths
func BenchmarkLeafSize64(b *testing.B) { LeafSizeHelper(b, 10, 64) }
-func LeafSizeHelper(b *testing.B, base Word, size int) {
+func LeafSizeHelper(b *testing.B, base, size int) {
b.StopTimer()
originalLeafSize := leafSize
resetTable(cacheBase10.table[:])
@@ -382,12 +379,12 @@
for d := 1; d <= 10000; d *= 10 {
b.StopTimer()
var z nat
- z = z.expWW(base, Word(d)) // build target number
- _ = z.string(lowercaseDigits[:base]) // warm divisor cache
+ z = z.expWW(Word(base), Word(d)) // build target number
+ _ = z.itoa(base) // warm divisor cache
b.StartTimer()
for i := 0; i < b.N; i++ {
- _ = z.string(lowercaseDigits[:base])
+ _ = z.itoa(base)
}
}
@@ -408,13 +405,13 @@
}
func TestStringPowers(t *testing.T) {
- var b, p Word
- for b = 2; b <= 16; b++ {
+ var p Word
+ for b := 2; b <= 16; b++ {
for p = 0; p <= 512; p++ {
- x := nat(nil).expWW(b, p)
- xs := x.string(lowercaseDigits[:b])
- xs2 := toString(x, lowercaseDigits[:b])
- if xs != xs2 {
+ x := nat(nil).expWW(Word(b), p)
+ xs := x.itoa(b)
+ xs2 := itoa(x, b)
+ if !bytes.Equal(xs, xs2) {
t.Errorf("failed at %d ** %d in base %d: %s != %s", b, p, b, xs, xs2)
}
}
diff --git a/src/math/big/ratconv.go b/src/math/big/ratconv.go
index 961ff64..8cf6716 100644
--- a/src/math/big/ratconv.go
+++ b/src/math/big/ratconv.go
@@ -190,7 +190,7 @@
func (x *Rat) String() string {
s := "/1"
if len(x.b.abs) != 0 {
- s = "/" + x.b.abs.decimalString()
+ s = "/" + string(x.b.abs.itoa(10))
}
return x.a.String() + s
}
@@ -237,13 +237,13 @@
}
}
- s := q.decimalString()
+ s := string(q.itoa(10))
if x.a.neg {
s = "-" + s
}
if prec > 0 {
- rs := r.decimalString()
+ rs := string(r.itoa(10))
leadingZeros := prec - len(rs)
s += "." + strings.Repeat("0", leadingZeros) + rs
}