Skip to content

Commit f4e138b

Browse files
authored
Faster add, etc. (#85)
- Make Add faster. - Add checks that most operations never allocate. - Remove `-count=10` from profiler make rules. - Rename `cmp` to `cmp64` to avoid collision with standard package. - Rework test suite ops as a map of functions. - Remove `errorf` helper (no value-add).
1 parent 49080fe commit f4e138b

10 files changed

+184
-140
lines changed

Makefile

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.PHONY: all
2-
all: test-all build-linux lint
2+
all: test-all build-linux lint no-allocs
33

44
.PHONY: ci
55
ci: test-all no-allocs
@@ -8,10 +8,13 @@ ci: test-all no-allocs
88
test-all: test test-32
99

1010
.PHONY: test
11-
test:
12-
go test $(GOTESTFLAGS)
11+
test: test-release
1312
go test $(GOTESTFLAGS) -tags=decimal_debug
1413

14+
.PHONY: test-release
15+
test-release:
16+
go test $(GOTESTFLAGS)
17+
1518
.PHONY: test-32
1619
test-32:
1720
if [ "$(shell go env GOOS)" = "linux" ]; then \
@@ -59,7 +62,7 @@ lint: build-linux
5962

6063
.INTERMEDIATE: %.prof
6164
%.prof: $(wildcard *.go)
62-
go test -$*profile $@ -count=10 $(GOPROFILEFLAGS)
65+
go test -$*profile $@ $(GOPROFILEFLAGS)
6366

6467
.PHONY: bench
6568
bench: bench.txt

decimal64.go

+1-11
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,7 @@ func renormalize(exp int16, significand uint64) (int16, uint64) {
222222
}
223223

224224
// roundStatus gives info about the truncated part of the significand that can't be fully stored in 16 decimal digits.
225-
func roundStatus(significand uint64, exp int16, targetExp int16) discardedDigit {
226-
expDiff := targetExp - exp
225+
func roundStatus(significand uint64, expDiff int16) discardedDigit {
227226
if expDiff > 19 && significand != 0 {
228227
return lt5
229228
}
@@ -530,15 +529,6 @@ func (d Decimal64) Class() string {
530529
return "+Normal-Normal"[7*dp.sign : 7*(dp.sign+1)]
531530
}
532531

533-
// numDecimalDigitsU64 returns the magnitude (number of digits) of a uint64.
534-
func numDecimalDigitsU64(n uint64) int16 {
535-
numDigits := int16(bits.Len64(n) * 77 / 256) // ~ 3/10
536-
if n >= tenToThe[uint(numDigits)%uint(len(tenToThe))] {
537-
numDigits++
538-
}
539-
return numDigits
540-
}
541-
542532
func checkNan(d, e Decimal64, dp, ep *decParts) (Decimal64, bool) {
543533
dp.fl = d.flavor()
544534
ep.fl = e.flavor()

decimal64_test.go

-10
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,6 @@ func TestDecimal64isZero(t *testing.T) {
315315
check(t, !One64.IsZero())
316316
}
317317

318-
func TestNumDecimalDigits(t *testing.T) {
319-
t.Parallel()
320-
321-
for i, num := range tenToThe {
322-
for j := uint64(1); j < 10 && i < 19; j++ {
323-
equal(t, i+1, int(numDecimalDigitsU64(num*j)))
324-
}
325-
}
326-
}
327-
328318
func TestIsSubnormal(t *testing.T) {
329319
t.Parallel()
330320

decimal64decParts.go

+42-12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,38 @@ func (ans *decParts) add128(dp, ep *decParts) {
3838
}
3939
}
4040

41+
// add64 adds the low 64 bits of two decParts
42+
func (ans *decParts) add64(dp, ep *decParts) {
43+
ans.exp = dp.exp
44+
switch {
45+
case dp.sign == ep.sign:
46+
ans.sign = dp.sign
47+
ans.significand.lo = dp.significand.lo + ep.significand.lo
48+
case dp.significand.lt(&ep.significand):
49+
ans.sign = ep.sign
50+
ans.significand.lo = ep.significand.lo - dp.significand.lo
51+
case ep.significand.lt(&dp.significand):
52+
ans.sign = dp.sign
53+
ans.significand.lo = dp.significand.lo - ep.significand.lo
54+
}
55+
}
56+
57+
// add128 adds two decParts with full precision in 128 bits of significand
58+
func (ans *decParts) add128V2(dp, ep *decParts) {
59+
ans.exp = dp.exp
60+
switch {
61+
case dp.sign == ep.sign:
62+
ans.sign = dp.sign
63+
ans.significand.add(&dp.significand, &ep.significand)
64+
case dp.significand.lt(&ep.significand):
65+
ans.sign = ep.sign
66+
ans.significand.sub(&ep.significand, &dp.significand)
67+
case ep.significand.lt(&dp.significand):
68+
ans.sign = dp.sign
69+
ans.significand.sub(&dp.significand, &ep.significand)
70+
}
71+
}
72+
4173
func (dp *decParts) matchScales128(ep *decParts) {
4274
expDiff := ep.exp - dp.exp
4375
if (ep.significand != uint128T{0, 0}) {
@@ -56,10 +88,10 @@ func (dp *decParts) roundToLo() discardedDigit {
5688

5789
if ds := &dp.significand; ds.hi > 0 || ds.lo >= 10*decimal64Base {
5890
var remainder uint64
59-
expDiff := ds.numDecimalDigits() - 16
91+
expDiff := int16(ds.numDecimalDigits()) - 16
6092
dp.exp += expDiff
6193
remainder = ds.divrem64(ds, tenToThe[expDiff])
62-
rndStatus = roundStatus(remainder, 0, expDiff)
94+
rndStatus = roundStatus(remainder, expDiff)
6395
}
6496
return rndStatus
6597
}
@@ -74,7 +106,9 @@ func (dp *decParts) isSubnormal() bool {
74106

75107
// separation gets the separation in decimal places of the MSD's of two decimal 64s
76108
func (dp *decParts) separation(ep *decParts) int16 {
77-
return dp.significand.numDecimalDigits() + dp.exp - ep.significand.numDecimalDigits() - ep.exp
109+
sep := int16(dp.significand.numDecimalDigits()) + dp.exp
110+
sep -= int16(ep.significand.numDecimalDigits()) + ep.exp
111+
return sep
78112
}
79113

80114
// removeZeros removes zeros and increments the exponent to match.
@@ -115,18 +149,17 @@ func (dp *decParts) isinf() bool {
115149
return dp.fl == flInf
116150
}
117151

118-
func (dp *decParts) rescale(targetExp int16) (rndStatus discardedDigit) {
152+
func (dp *decParts) rescale(targetExp int16) discardedDigit {
119153
expDiff := targetExp - dp.exp
120-
mag := dp.significand.numDecimalDigits()
121-
rndStatus = roundStatus(dp.significand.lo, dp.exp, targetExp)
122-
if expDiff > mag {
154+
rndStatus := roundStatus(dp.significand.lo, expDiff)
155+
if expDiff > int16(dp.significand.numDecimalDigits()) {
123156
dp.significand.lo, dp.exp = 0, targetExp
124-
return
157+
return rndStatus
125158
}
126159
divisor := tenToThe[expDiff]
127160
dp.significand.lo = dp.significand.lo / divisor
128161
dp.exp = targetExp
129-
return
162+
return rndStatus
130163
}
131164

132165
func (dp *decParts) unpack(d Decimal64) {
@@ -142,9 +175,6 @@ func (dp *decParts) unpackV2(d Decimal64) {
142175
// EE ∈ {00, 01, 10}
143176
dp.exp = int16((d.bits>>(63-10))&(1<<10-1)) - expOffset
144177
dp.significand.lo = d.bits & (1<<53 - 1)
145-
if dp.significand.lo == 0 {
146-
dp.exp = 0
147-
}
148178
case flNormal51:
149179
// s 11EEeeeeeeee (100)t tttttttttt tttttttttt tttttttttt tttttttttt tttttttttt
150180
// EE ∈ {00, 01, 10}

decimal64math.go

+23-10
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (d Decimal64) Cmp(e Decimal64) int {
6161
if _, nan := checkNan(d, e, &dp, &ep); nan {
6262
return -2
6363
}
64-
return cmp(d, e, &dp, &ep)
64+
return cmp64(d, e, &dp, &ep)
6565
}
6666

6767
// Cmp64 returns the same output as Cmp as a Decimal64, unless d or e is NaN, in
@@ -71,7 +71,7 @@ func (d Decimal64) Cmp64(e Decimal64) Decimal64 {
7171
if nan, is := checkNan(d, e, &dp, &ep); is {
7272
return nan
7373
}
74-
switch cmp(d, e, &dp, &ep) {
74+
switch cmp64(d, e, &dp, &ep) {
7575
case -1:
7676
return NegOne64
7777
case 1:
@@ -81,9 +81,9 @@ func (d Decimal64) Cmp64(e Decimal64) Decimal64 {
8181
}
8282
}
8383

84-
func cmp(d, e Decimal64, dp, ep *decParts) int {
84+
func cmp64(d, e Decimal64, dp, ep *decParts) int {
8585
switch {
86-
case dp.isZero() && ep.isZero(), d == e:
86+
case d == e, dp.isZero() && ep.isZero():
8787
return 0
8888
default:
8989
diff := d.Sub(e)
@@ -112,7 +112,7 @@ func (d Decimal64) min(e Decimal64, sign int) Decimal64 {
112112

113113
switch {
114114
case !dnan && !enan: // Fast path for non-NaNs.
115-
if sign*cmp(d, e, &dp, &ep) < 0 {
115+
if sign*cmp64(d, e, &dp, &ep) < 0 {
116116
return d
117117
}
118118
return e
@@ -152,7 +152,7 @@ func (d Decimal64) minMag(e Decimal64, sign int) Decimal64 {
152152

153153
switch {
154154
case !dnan && !enan: // Fast path for non-NaNs.
155-
switch sign * cmp(da, ea, &dp, &ep) {
155+
switch sign * cmp64(da, ea, &dp, &ep) {
156156
case -1:
157157
return d
158158
case 1:
@@ -334,7 +334,7 @@ func (ctx Context64) add(d, e Decimal64, dp, ep *decParts) Decimal64 {
334334
} else if ep.significand.lo == 0 {
335335
return d
336336
}
337-
sep := dp.separation(ep)
337+
sep := dp.exp - ep.exp
338338

339339
if sep < -17 {
340340
return e
@@ -348,13 +348,26 @@ func (ctx Context64) add(d, e Decimal64, dp, ep *decParts) Decimal64 {
348348
}
349349
var rndStatus discardedDigit
350350
var ans decParts
351-
ans.add128(dp, ep)
351+
switch {
352+
case sep == 0:
353+
ans.add64(dp, ep)
354+
case sep < 4:
355+
dp.significand.lo *= tenToThe[sep]
356+
dp.exp -= sep
357+
ans.add64(dp, ep)
358+
default:
359+
dp.significand.mul64(&dp.significand, tenToThe[17])
360+
dp.exp -= 17
361+
ep.significand.mul64(&ep.significand, tenToThe[17-sep])
362+
ep.exp -= 17 - sep
363+
ans.add128V2(dp, ep)
364+
}
352365
rndStatus = ans.roundToLo()
353366
if ans.exp < -expOffset {
354367
rndStatus = ans.rescale(-expOffset)
355368
}
356369
ans.significand.lo = ctx.Rounding.round(ans.significand.lo, rndStatus)
357-
if ans.exp >= -expOffset && ans.significand.lo != 0 {
370+
if ans.significand.lo != 0 {
358371
ans.exp, ans.significand.lo = renormalize(ans.exp, ans.significand.lo)
359372
}
360373
if ans.exp > expMax || ans.significand.lo > maxSig {
@@ -365,7 +378,7 @@ func (ctx Context64) add(d, e Decimal64, dp, ep *decParts) Decimal64 {
365378

366379
// Add computes d + e
367380
func (ctx Context64) Sub(d, e Decimal64) Decimal64 {
368-
return d.Add(e.Neg())
381+
return d.Add(new64(neg64 ^ e.bits))
369382
}
370383

371384
// FMA computes d*e + f

decimal64math_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ func TestDecimal64Add(t *testing.T) {
4848
func(a, b int64) int64 { return a + b },
4949
func(a, b Decimal64) Decimal64 { return a.Add(b) },
5050
)
51+
52+
add := func(a, b, expected string, ctx *Context64) func(*testing.T) {
53+
return func(*testing.T) {
54+
t.Helper()
55+
56+
e := MustParse64(expected)
57+
x := MustParse64(a)
58+
y := MustParse64(b)
59+
if ctx == nil {
60+
ctx = &DefaultContext64
61+
}
62+
replayOnFail(t, func() {
63+
z := ctx.Add(x, y)
64+
equalD64(t, e, z)
65+
})
66+
}
67+
}
68+
69+
t.Run("tiny-neg", add("1E-383", "-1E-398", "9.99999999999999E-384", nil))
70+
71+
he := Context64{Rounding: HalfEven}
72+
t.Run("round-even", add("12345678", "0.123456785", "12345678.12345678", &he))
5173
}
5274

5375
func TestDecimal64AddNaN(t *testing.T) {

0 commit comments

Comments
 (0)