Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster add, etc. #85

Merged
merged 5 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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