Skip to content

Commit

Permalink
Faster add, etc. (#85)
Browse files Browse the repository at this point in the history
- 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).
  • Loading branch information
anzdaddy authored Nov 28, 2024
1 parent 49080fe commit f4e138b
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 140 deletions.
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: all
all: test-all build-linux lint
all: test-all build-linux lint no-allocs

.PHONY: ci
ci: test-all no-allocs
Expand All @@ -8,10 +8,13 @@ ci: test-all no-allocs
test-all: test test-32

.PHONY: test
test:
go test $(GOTESTFLAGS)
test: test-release
go test $(GOTESTFLAGS) -tags=decimal_debug

.PHONY: test-release
test-release:
go test $(GOTESTFLAGS)

.PHONY: test-32
test-32:
if [ "$(shell go env GOOS)" = "linux" ]; then \
Expand Down Expand Up @@ -59,7 +62,7 @@ lint: build-linux

.INTERMEDIATE: %.prof
%.prof: $(wildcard *.go)
go test -$*profile $@ -count=10 $(GOPROFILEFLAGS)
go test -$*profile $@ $(GOPROFILEFLAGS)

.PHONY: bench
bench: bench.txt
Expand Down
12 changes: 1 addition & 11 deletions decimal64.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,7 @@ func renormalize(exp int16, significand uint64) (int16, uint64) {
}

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

// numDecimalDigitsU64 returns the magnitude (number of digits) of a uint64.
func numDecimalDigitsU64(n uint64) int16 {
numDigits := int16(bits.Len64(n) * 77 / 256) // ~ 3/10
if n >= tenToThe[uint(numDigits)%uint(len(tenToThe))] {
numDigits++
}
return numDigits
}

func checkNan(d, e Decimal64, dp, ep *decParts) (Decimal64, bool) {
dp.fl = d.flavor()
ep.fl = e.flavor()
Expand Down
10 changes: 0 additions & 10 deletions decimal64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,16 +315,6 @@ func TestDecimal64isZero(t *testing.T) {
check(t, !One64.IsZero())
}

func TestNumDecimalDigits(t *testing.T) {
t.Parallel()

for i, num := range tenToThe {
for j := uint64(1); j < 10 && i < 19; j++ {
equal(t, i+1, int(numDecimalDigitsU64(num*j)))
}
}
}

func TestIsSubnormal(t *testing.T) {
t.Parallel()

Expand Down
54 changes: 42 additions & 12 deletions decimal64decParts.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@ func (ans *decParts) add128(dp, ep *decParts) {
}
}

// add64 adds the low 64 bits of two decParts
func (ans *decParts) add64(dp, ep *decParts) {
ans.exp = dp.exp
switch {
case dp.sign == ep.sign:
ans.sign = dp.sign
ans.significand.lo = dp.significand.lo + ep.significand.lo
case dp.significand.lt(&ep.significand):
ans.sign = ep.sign
ans.significand.lo = ep.significand.lo - dp.significand.lo
case ep.significand.lt(&dp.significand):
ans.sign = dp.sign
ans.significand.lo = dp.significand.lo - ep.significand.lo
}
}

// add128 adds two decParts with full precision in 128 bits of significand
func (ans *decParts) add128V2(dp, ep *decParts) {
ans.exp = dp.exp
switch {
case dp.sign == ep.sign:
ans.sign = dp.sign
ans.significand.add(&dp.significand, &ep.significand)
case dp.significand.lt(&ep.significand):
ans.sign = ep.sign
ans.significand.sub(&ep.significand, &dp.significand)
case ep.significand.lt(&dp.significand):
ans.sign = dp.sign
ans.significand.sub(&dp.significand, &ep.significand)
}
}

func (dp *decParts) matchScales128(ep *decParts) {
expDiff := ep.exp - dp.exp
if (ep.significand != uint128T{0, 0}) {
Expand All @@ -56,10 +88,10 @@ func (dp *decParts) roundToLo() discardedDigit {

if ds := &dp.significand; ds.hi > 0 || ds.lo >= 10*decimal64Base {
var remainder uint64
expDiff := ds.numDecimalDigits() - 16
expDiff := int16(ds.numDecimalDigits()) - 16
dp.exp += expDiff
remainder = ds.divrem64(ds, tenToThe[expDiff])
rndStatus = roundStatus(remainder, 0, expDiff)
rndStatus = roundStatus(remainder, expDiff)
}
return rndStatus
}
Expand All @@ -74,7 +106,9 @@ func (dp *decParts) isSubnormal() bool {

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

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

func (dp *decParts) rescale(targetExp int16) (rndStatus discardedDigit) {
func (dp *decParts) rescale(targetExp int16) discardedDigit {
expDiff := targetExp - dp.exp
mag := dp.significand.numDecimalDigits()
rndStatus = roundStatus(dp.significand.lo, dp.exp, targetExp)
if expDiff > mag {
rndStatus := roundStatus(dp.significand.lo, expDiff)
if expDiff > int16(dp.significand.numDecimalDigits()) {
dp.significand.lo, dp.exp = 0, targetExp
return
return rndStatus
}
divisor := tenToThe[expDiff]
dp.significand.lo = dp.significand.lo / divisor
dp.exp = targetExp
return
return rndStatus
}

func (dp *decParts) unpack(d Decimal64) {
Expand All @@ -142,9 +175,6 @@ func (dp *decParts) unpackV2(d Decimal64) {
// EE ∈ {00, 01, 10}
dp.exp = int16((d.bits>>(63-10))&(1<<10-1)) - expOffset
dp.significand.lo = d.bits & (1<<53 - 1)
if dp.significand.lo == 0 {
dp.exp = 0
}
case flNormal51:
// s 11EEeeeeeeee (100)t tttttttttt tttttttttt tttttttttt tttttttttt tttttttttt
// EE ∈ {00, 01, 10}
Expand Down
33 changes: 23 additions & 10 deletions decimal64math.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (d Decimal64) Cmp(e Decimal64) int {
if _, nan := checkNan(d, e, &dp, &ep); nan {
return -2
}
return cmp(d, e, &dp, &ep)
return cmp64(d, e, &dp, &ep)
}

// Cmp64 returns the same output as Cmp as a Decimal64, unless d or e is NaN, in
Expand All @@ -71,7 +71,7 @@ func (d Decimal64) Cmp64(e Decimal64) Decimal64 {
if nan, is := checkNan(d, e, &dp, &ep); is {
return nan
}
switch cmp(d, e, &dp, &ep) {
switch cmp64(d, e, &dp, &ep) {
case -1:
return NegOne64
case 1:
Expand All @@ -81,9 +81,9 @@ func (d Decimal64) Cmp64(e Decimal64) Decimal64 {
}
}

func cmp(d, e Decimal64, dp, ep *decParts) int {
func cmp64(d, e Decimal64, dp, ep *decParts) int {
switch {
case dp.isZero() && ep.isZero(), d == e:
case d == e, dp.isZero() && ep.isZero():
return 0
default:
diff := d.Sub(e)
Expand Down Expand Up @@ -112,7 +112,7 @@ func (d Decimal64) min(e Decimal64, sign int) Decimal64 {

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

switch {
case !dnan && !enan: // Fast path for non-NaNs.
switch sign * cmp(da, ea, &dp, &ep) {
switch sign * cmp64(da, ea, &dp, &ep) {
case -1:
return d
case 1:
Expand Down Expand Up @@ -334,7 +334,7 @@ func (ctx Context64) add(d, e Decimal64, dp, ep *decParts) Decimal64 {
} else if ep.significand.lo == 0 {
return d
}
sep := dp.separation(ep)
sep := dp.exp - ep.exp

if sep < -17 {
return e
Expand All @@ -348,13 +348,26 @@ func (ctx Context64) add(d, e Decimal64, dp, ep *decParts) Decimal64 {
}
var rndStatus discardedDigit
var ans decParts
ans.add128(dp, ep)
switch {
case sep == 0:
ans.add64(dp, ep)
case sep < 4:
dp.significand.lo *= tenToThe[sep]
dp.exp -= sep
ans.add64(dp, ep)
default:
dp.significand.mul64(&dp.significand, tenToThe[17])
dp.exp -= 17
ep.significand.mul64(&ep.significand, tenToThe[17-sep])
ep.exp -= 17 - sep
ans.add128V2(dp, ep)
}
rndStatus = ans.roundToLo()
if ans.exp < -expOffset {
rndStatus = ans.rescale(-expOffset)
}
ans.significand.lo = ctx.Rounding.round(ans.significand.lo, rndStatus)
if ans.exp >= -expOffset && ans.significand.lo != 0 {
if ans.significand.lo != 0 {
ans.exp, ans.significand.lo = renormalize(ans.exp, ans.significand.lo)
}
if ans.exp > expMax || ans.significand.lo > maxSig {
Expand All @@ -365,7 +378,7 @@ func (ctx Context64) add(d, e Decimal64, dp, ep *decParts) Decimal64 {

// Add computes d + e
func (ctx Context64) Sub(d, e Decimal64) Decimal64 {
return d.Add(e.Neg())
return d.Add(new64(neg64 ^ e.bits))
}

// FMA computes d*e + f
Expand Down
22 changes: 22 additions & 0 deletions decimal64math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ func TestDecimal64Add(t *testing.T) {
func(a, b int64) int64 { return a + b },
func(a, b Decimal64) Decimal64 { return a.Add(b) },
)

add := func(a, b, expected string, ctx *Context64) func(*testing.T) {
return func(*testing.T) {
t.Helper()

e := MustParse64(expected)
x := MustParse64(a)
y := MustParse64(b)
if ctx == nil {
ctx = &DefaultContext64
}
replayOnFail(t, func() {
z := ctx.Add(x, y)
equalD64(t, e, z)
})
}
}

t.Run("tiny-neg", add("1E-383", "-1E-398", "9.99999999999999E-384", nil))

he := Context64{Rounding: HalfEven}
t.Run("round-even", add("12345678", "0.123456785", "12345678.12345678", &he))
}

func TestDecimal64AddNaN(t *testing.T) {
Expand Down
Loading

0 comments on commit f4e138b

Please sign in to comment.