Skip to content

Commit dd0c83a

Browse files
authored
performance fixes (#81)
- Much faster `Quo` - `Int64x` same as `Int64` but reports whether the conversion was exact. - `ScaleB`/`ScaleBInt` return a * 10^b - Precook small integers [-10, 10]. - `make profile` convenience rule
1 parent 17842dd commit dd0c83a

15 files changed

+276
-153
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ lint: build-linux
3333
-v `go env GOMODCACHE`:/go/pkg/mod \
3434
golangci/golangci-lint:v1.60.1-alpine \
3535
golangci-lint run
36+
37+
.PHONY: profile
38+
profile:
39+
go test -cpuprofile cpu.prof -count=10 && go tool pprof -http=:8080 cpu.prof

decimal64.go

+112-21
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,39 @@ func signalNaN64() {
109109
panic("sNaN64")
110110
}
111111

112-
func checkSignificandIsNormal(significand uint64) {
113-
logicCheck(decimal64Base <= significand, "%d <= %d", decimal64Base, significand)
114-
logicCheck(significand < 10*decimal64Base, "%d < %d", significand, 10*decimal64Base)
112+
var small64s = [...]Decimal64{
113+
new64FromInt64(-10),
114+
new64FromInt64(-9),
115+
new64FromInt64(-8),
116+
new64FromInt64(-7),
117+
new64FromInt64(-6),
118+
new64FromInt64(-5),
119+
new64FromInt64(-4),
120+
new64FromInt64(-3),
121+
new64FromInt64(-2),
122+
NegOne64,
123+
Zero64,
124+
One64,
125+
new64FromInt64(2),
126+
new64FromInt64(3),
127+
new64FromInt64(4),
128+
new64FromInt64(5),
129+
new64FromInt64(6),
130+
new64FromInt64(7),
131+
new64FromInt64(8),
132+
new64FromInt64(9),
133+
new64FromInt64(10),
115134
}
116135

117136
// New64FromInt64 returns a new Decimal64 with the given value.
118-
func New64FromInt64(value int64) Decimal64 {
119-
if value == 0 {
120-
return Zero64
137+
func New64FromInt64(i int64) Decimal64 {
138+
if i >= -10 && i <= 10 {
139+
return small64s[10+i]
121140
}
141+
return new64FromInt64(i)
142+
}
143+
144+
func new64FromInt64(value int64) Decimal64 {
122145
sign := 0
123146
if value < 0 {
124147
sign = 1
@@ -182,21 +205,25 @@ func roundStatus(significand uint64, exp int, targetExp int) discardedDigit {
182205
// TODO: make this more efficent
183206
func countTrailingZeros(n uint64) int {
184207
zeros := 0
185-
if n%10000000000000000 == 0 {
208+
q := n / 1_0000_0000_0000_0000
209+
if n == q*1_0000_0000_0000_0000 {
186210
zeros += 16
187-
n /= 10000000000000000
211+
n = q
188212
}
189-
if n%100000000 == 0 {
213+
q = n / 1_0000_0000
214+
if n == q*1_0000_0000 {
190215
zeros += 8
191-
n /= 100000000
216+
n = q
192217
}
193-
if n%10000 == 0 {
218+
q = n / 10000
219+
if n == q*10000 {
194220
zeros += 4
195-
n /= 10000
221+
n = q
196222
}
197-
if n%100 == 0 {
223+
q = n / 100
224+
if n == q*100 {
198225
zeros += 2
199-
n /= 100
226+
n = q
200227
}
201228
if n%10 == 0 {
202229
zeros++
@@ -315,28 +342,36 @@ func (d Decimal64) Float64() float64 {
315342

316343
// Int64 returns an int64 representation of d, clamped to [[math.MinInt64], [math.MaxInt64]].
317344
func (d Decimal64) Int64() int64 {
345+
i, _ := d.Int64x()
346+
return i
347+
}
348+
349+
// Int64 returns an int64 representation of d, clamped to [[math.MinInt64],
350+
// [math.MaxInt64]].
351+
// The second return value, exact indicates whether New64FromInt64(i) == d.
352+
func (d Decimal64) Int64x() (i int64, exact bool) {
318353
fl, sign, exp, significand := d.parts()
319354
switch fl {
320355
case flInf:
321356
if sign == 0 {
322-
return math.MaxInt64
357+
return math.MaxInt64, false
323358
}
324-
return math.MinInt64
359+
return math.MinInt64, false
325360
case flQNaN:
326-
return 0
361+
return 0, false
327362
case flSNaN:
328363
signalNaN64()
329-
return 0
364+
return 0, false
330365
}
331-
exp, whole, _ := expWholeFrac(exp, significand)
366+
exp, whole, frac := expWholeFrac(exp, significand)
332367
for exp > 0 && whole < math.MaxInt64/10 {
333368
exp--
334369
whole *= 10
335370
}
336371
if exp > 0 {
337-
return math.MaxInt64
372+
return math.MaxInt64, false
338373
}
339-
return int64(1-2*sign) * int64(whole)
374+
return int64(1-2*sign) * int64(whole), frac == 0
340375
}
341376

342377
// IsZero returns true if the Decimal encodes a zero value.
@@ -405,6 +440,62 @@ func (d Decimal64) Signbit() bool {
405440
return d.bits>>63 == 1
406441
}
407442

443+
func (d Decimal64) ScaleB(e Decimal64) Decimal64 {
444+
var dp decParts
445+
dp.unpack(d)
446+
var ep decParts
447+
ep.unpack(e)
448+
if r, nan := checkNan(&dp, &ep); nan {
449+
return r
450+
}
451+
452+
if dp.fl != flNormal || dp.isZero() {
453+
return d
454+
}
455+
if ep.fl != flNormal {
456+
return QNaN64
457+
}
458+
459+
i, exact := e.Int64x()
460+
if !exact {
461+
return QNaN64
462+
}
463+
return scaleBInt(&dp, int(i))
464+
}
465+
466+
func (d Decimal64) ScaleBInt(i int) Decimal64 {
467+
var dp decParts
468+
dp.unpack(d)
469+
if dp.fl != flNormal || dp.isZero() {
470+
return d
471+
}
472+
return scaleBInt(&dp, i)
473+
}
474+
475+
func scaleBInt(dp *decParts, i int) Decimal64 {
476+
dp.exp += i
477+
478+
for dp.significand.lo < decimal64Base && dp.exp > -expOffset {
479+
dp.exp--
480+
dp.significand.lo *= 10
481+
}
482+
483+
switch {
484+
case dp.exp > expMax:
485+
return Infinity64.CopySign(dp.original)
486+
case dp.exp < -expOffset:
487+
for dp.exp < -expOffset {
488+
dp.exp++
489+
dp.significand.lo /= 10
490+
}
491+
if dp.significand.lo == 0 {
492+
return Zero64.CopySign(dp.original)
493+
}
494+
}
495+
496+
return dp.decimal64()
497+
}
498+
408499
// Class returns a string representing the number's 'type' that the decimal is.
409500
// It can be one of the following:
410501
//

decimal64_debug.go

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
package decimal
55

6+
import "fmt"
7+
68
// Decimal64 represents an IEEE 754 64-bit floating point decimal number.
79
// It uses the binary representation method.
810
// Decimal64 is intentionally a struct to ensure users don't accidentally cast it to uint64.
@@ -32,3 +34,13 @@ func new64(bits uint64) Decimal64 {
3234

3335
return d
3436
}
37+
38+
func checkSignificandIsNormal(significand uint64) {
39+
if decimal64Base > significand {
40+
panic(fmt.Errorf("Failed logic check: %d <= %d", decimal64Base, significand))
41+
}
42+
43+
if significand >= 10*decimal64Base {
44+
panic(fmt.Errorf("Failed logic check: %d < %d", significand, 10*decimal64Base))
45+
}
46+
}

decimal64_ndebug.go

+2
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ type Decimal64 struct {
1616
func new64(bits uint64) Decimal64 {
1717
return new64Raw(bits)
1818
}
19+
20+
func checkSignificandIsNormal(significand uint64) {}

decimal64decParts.go

+9-15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ func unpack(d Decimal64) decParts {
1515
return dp
1616
}
1717

18+
func (dp *decParts) decimal64() Decimal64 {
19+
return newFromParts(dp.sign, dp.exp, dp.significand.lo)
20+
}
21+
1822
// add128 adds two decParts with full precision in 128 bits of significand
1923
func (dp *decParts) add128(ep *decParts) decParts {
2024
dp.matchScales128(ep)
@@ -24,7 +28,7 @@ func (dp *decParts) add128(ep *decParts) decParts {
2428
ans.sign = dp.sign
2529
ans.significand = dp.significand.add(ep.significand)
2630
} else {
27-
if ep.significand.gt(dp.significand) {
31+
if dp.significand.lt(ep.significand) {
2832
ans.sign = ep.sign
2933
ans.significand = ep.significand.sub(dp.significand)
3034
} else if ep.significand.lt(dp.significand) {
@@ -50,24 +54,14 @@ func (dp *decParts) matchScales128(ep *decParts) {
5054
}
5155
}
5256

53-
func (dp *decParts) matchSignificandDigits(ep *decParts) {
54-
expDiff := ep.significand.numDecimalDigits() - dp.significand.numDecimalDigits()
55-
if expDiff < 0 {
56-
ep.significand = ep.significand.mul(tenToThe128[-expDiff-1])
57-
ep.exp -= -expDiff - 1
58-
return
59-
}
60-
dp.significand = dp.significand.mul(tenToThe128[expDiff+1])
61-
dp.exp -= expDiff + 1
62-
}
63-
6457
func (dp *decParts) roundToLo() discardedDigit {
6558
var rndStatus discardedDigit
66-
if dp.significand.numDecimalDigits() > 16 {
59+
60+
if dsig := dp.significand; dsig.hi > 0 || dsig.lo >= 10*decimal64Base {
6761
var remainder uint64
68-
expDiff := dp.significand.numDecimalDigits() - 16
62+
expDiff := dsig.numDecimalDigits() - 16
6963
dp.exp += expDiff
70-
dp.significand, remainder = dp.significand.divrem64(tenToThe[expDiff])
64+
dp.significand, remainder = dsig.divrem64(tenToThe[expDiff])
7165
rndStatus = roundStatus(remainder, 0, expDiff)
7266
}
7367
return rndStatus

decimal64math.go

+37-35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package decimal
22

3+
import "math/bits"
4+
35
// Equal indicates whether two numbers are equal.
46
// It is equivalent to d.Cmp(e) == 0.
57
func (d Decimal64) Equal(e Decimal64) bool {
@@ -240,43 +242,43 @@ func (ctx Context64) Quo(d, e Decimal64) Decimal64 {
240242
if ep.isZero() {
241243
return infinities64[ans.sign]
242244
}
243-
dp.matchSignificandDigits(&ep)
244-
ans.exp = dp.exp - ep.exp
245-
for {
246-
for dp.significand.gt(ep.significand) {
247-
dp.significand = dp.significand.sub(ep.significand)
248-
ans.significand = ans.significand.add(uint128T{1, 0})
249-
}
250-
if dp.significand == (uint128T{}) || ans.significand.numDecimalDigits() == 16 {
251-
break
252-
}
253-
ans.significand = ans.significand.mulBy10()
254-
dp.significand = dp.significand.mulBy10()
255-
ans.exp--
256-
}
257-
var rndStatus discardedDigit
258-
dp.significand = dp.significand.mul64(2)
259-
if dp.significand == (uint128T{}) {
260-
rndStatus = eq0
261-
} else if dp.significand.gt(ep.significand) {
262-
rndStatus = gt5
263-
} else if dp.significand.lt(ep.significand) {
264-
rndStatus = lt5
265-
} else {
266-
rndStatus = eq5
245+
246+
const ampl = 1000
247+
hi, lo := bits.Mul64(dp.significand.lo, ampl*decimal64Base)
248+
q, _ := bits.Div64(hi, lo, ep.significand.lo)
249+
exp := dp.exp - ep.exp - 15
250+
251+
for q < ampl*decimal64Base && exp > -expOffset {
252+
q *= 10
253+
exp--
267254
}
268-
ans.significand.lo = ctx.Rounding.round(ans.significand.lo, rndStatus)
269-
if ans.exp < -expOffset {
270-
rndStatus = ans.rescale(-expOffset)
271-
ans.significand.lo = ctx.Rounding.round(ans.significand.lo, rndStatus)
255+
for q >= 10*ampl*decimal64Base {
256+
q /= 10
257+
exp++
272258
}
273-
if ans.exp >= -expOffset && ans.significand.lo != 0 {
274-
ans.exp, ans.significand.lo = renormalize(ans.exp, ans.significand.lo)
259+
for exp < -expOffset {
260+
q /= 10
261+
exp++
275262
}
276-
if ans.significand.lo > maxSig || ans.exp > expMax {
263+
if exp > expMax {
277264
return infinities64[ans.sign]
278265
}
279-
return newFromParts(ans.sign, ans.exp, ans.significand.lo)
266+
267+
switch ctx.Rounding {
268+
case HalfUp:
269+
q = (q + ampl/2) / ampl
270+
case HalfEven:
271+
d := q / ampl
272+
rem := q - d*ampl
273+
q = d
274+
if rem > ampl/2 || rem == ampl/2 && d%2 == 1 {
275+
q++
276+
}
277+
case Down:
278+
q /= ampl
279+
}
280+
281+
return newFromParts(ans.sign, exp, q)
280282
}
281283

282284
// Sqrt computes √d.
@@ -357,7 +359,7 @@ func (ctx Context64) Add(d, e Decimal64) Decimal64 {
357359
if ans.exp > expMax || ans.significand.lo > maxSig {
358360
return infinities64[ans.sign]
359361
}
360-
return newFromParts(ans.sign, ans.exp, ans.significand.lo)
362+
return ans.decimal64()
361363
}
362364

363365
// Add computes d + e
@@ -418,7 +420,7 @@ func (ctx Context64) FMA(d, e, f Decimal64) Decimal64 {
418420
if ans.exp > expMax || ans.significand.lo > maxSig {
419421
return infinities64[ans.sign]
420422
}
421-
return newFromParts(ans.sign, ans.exp, ans.significand.lo)
423+
return ans.decimal64()
422424
}
423425

424426
// Mul computes d * e
@@ -454,7 +456,7 @@ func (ctx Context64) Mul(d, e Decimal64) Decimal64 {
454456
if ans.significand.lo > maxSig || ans.exp > expMax {
455457
return infinities64[ans.sign]
456458
}
457-
return newFromParts(ans.sign, ans.exp, ans.significand.lo)
459+
return ans.decimal64()
458460
}
459461

460462
// NextPlus returns the next value above d.

0 commit comments

Comments
 (0)