Skip to content

Commit

Permalink
apd: eliminate escape analysis barrier around Rounder
Browse files Browse the repository at this point in the history
This commit reworks the `Rounder` API to eliminate the escape analysis
barrier that its was creating, which was resulting in unnecessary heap
allocations. The commit replaces the opaque functions used for dynamic
dispatch with a switch statement, which escape analysis is more easily
able to understand.

Unfortunately, to do this, we need to make the roundings a closed set
and remove the ability for users to supply their own rounding routines.
I think this is a reasonable trade-off, given that we are not aware of
anyone actually using the extra flexibility.

Before
```
➜ goescape . | grep moved
./decimal.go:325:8: moved to heap: integ
./round.go:73:7: moved to heap: y
./context.go:287:6: moved to heap: quo
```

After
```
➜ goescape . | grep moved | wc -l
       0
```
  • Loading branch information
nvanbenschoten committed Jan 4, 2022
1 parent 0da8865 commit c951ca9
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 49 deletions.
8 changes: 4 additions & 4 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type Context struct {
Traps Condition
// Rounding specifies the Rounder to use during rounding. RoundHalfUp is used if
// empty or not present in Roundings.
Rounding string
Rounding Rounder
}

const (
Expand Down Expand Up @@ -351,8 +351,7 @@ func (c *Context) Quo(d, x, y *Decimal) (Condition, error) {
res |= Inexact | Rounded
dividend.Mul(&dividend, bigTwo)
half := dividend.Cmp(&divisor)
rounding := c.rounding()
if rounding(&quo.Coeff, quo.Negative, half) {
if c.Rounding.ShouldAddOne(&quo.Coeff, quo.Negative, half) {
roundAddOne(&quo.Coeff, &diff)
}
}
Expand Down Expand Up @@ -1203,7 +1202,7 @@ func (c *Context) quantize(d, v *Decimal, exp int32) Condition {

d.Exponent = -diff
// Avoid the c.Precision == 0 check.
res = nc.rounding().Round(nc, d, d)
res = nc.Rounding.Round(nc, d, d)
// Adjust for 0.9 -> 1.0 rollover.
if d.Exponent > 0 {
d.Coeff.Mul(&d.Coeff, bigTen)
Expand Down Expand Up @@ -1284,6 +1283,7 @@ func (c *Context) Reduce(d, x *Decimal) (int, Condition, error) {
}

// exp10 returns x, 10^x. An error is returned if x is too large.
// The returned value must not be mutated.
func exp10(x int64, tmp *BigInt) (exp *BigInt, err error) {
if x > MaxExponent || x < MinExponent {
return nil, errors.New(errExponentOutOfRangeStr)
Expand Down
2 changes: 1 addition & 1 deletion decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ func (d *Decimal) setExponent(c *Context, res Condition, xs ...int64) Condition
frac.Abs(&frac)
if !frac.IsZero() {
res |= Inexact
if c.rounding()(&integ.Coeff, integ.Negative, frac.Cmp(decimalHalf)) {
if c.Rounding.ShouldAddOne(&integ.Coeff, integ.Negative, frac.Cmp(decimalHalf)) {
integ.Coeff.Add(&integ.Coeff, bigOne)
}
}
Expand Down
10 changes: 3 additions & 7 deletions gda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,15 +353,15 @@ func readGDA(t testing.TB, name string) (string, []TestCase) {
}

func (tc TestCase) Context(t testing.TB) *Context {
_, ok := Roundings[tc.Rounding]
if !ok {
rounding := Rounder(tc.Rounding)
if _, ok := roundings[rounding]; !ok {
t.Fatalf("unsupported rounding mode %s", tc.Rounding)
}
c := &Context{
Precision: uint32(tc.Precision),
MaxExponent: int32(tc.MaxExponent),
MinExponent: int32(tc.MinExponent),
Rounding: tc.Rounding,
Rounding: rounding,
Traps: 0,
}
return c
Expand Down Expand Up @@ -410,10 +410,6 @@ func gdaTest(t *testing.T, path string, tcs []TestCase) {
t.Logf("%s:/^%s ", path, tc.ID)
t.Logf("%s %s = %s (%s)", tc.Operation, strings.Join(tc.Operands, " "), tc.Result, strings.Join(tc.Conditions, " "))
t.Logf("prec: %d, round: %s, Emax: %d, Emin: %d", tc.Precision, tc.Rounding, tc.MaxExponent, tc.MinExponent)
_, ok := Roundings[tc.Rounding]
if !ok {
t.Fatalf("unsupported rounding mode %s", tc.Rounding)
}
operands := make([]*Decimal, 2)
c := tc.Context(t)
var res, opres Condition
Expand Down
87 changes: 50 additions & 37 deletions round.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,42 @@ func (c *Context) round(d, x *Decimal) Condition {
d.Set(x)
return d.setExponent(c, 0, int64(d.Exponent))
}
rounder := c.rounding()
res := rounder.Round(c, d, x)
res := c.Rounding.Round(c, d, x)
return res
}

func (c *Context) rounding() Rounder {
rounding, ok := Roundings[c.Rounding]
if !ok {
return roundHalfUp
// Rounder specifies the behavior of rounding.
type Rounder string

// ShouldAddOne returns true if 1 should be added to the absolute value
// of a number being rounded. result is the result to which the 1 would
// be added. neg is true if the number is negative. half is -1 if the
// discarded digits are < 0.5, 0 if = 0.5, or 1 if > 0.5.
func (r Rounder) ShouldAddOne(result *BigInt, neg bool, half int) bool {
// NOTE: this is written using a switch statement instead of some
// other form of dynamic dispatch to assist Go's escape analysis.
switch r {
case RoundDown:
return roundDown(result, neg, half)
case RoundHalfUp:
return roundHalfUp(result, neg, half)
case RoundHalfEven:
return roundHalfEven(result, neg, half)
case RoundCeiling:
return roundCeiling(result, neg, half)
case RoundFloor:
return roundFloor(result, neg, half)
case RoundHalfDown:
return roundHalfDown(result, neg, half)
case RoundUp:
return roundUp(result, neg, half)
case Round05Up:
return round05Up(result, neg, half)
default:
return roundHalfUp(result, neg, half)
}
return rounding
}

// Rounder defines a function that returns true if 1 should be added to the
// absolute value of a number being rounded. result is the result to which
// the 1 would be added. neg is true if the number is negative. half is -1
// if the discarded digits are < 0.5, 0 if = 0.5, or 1 if > 0.5.
type Rounder func(result *BigInt, neg bool, half int) bool

// Round sets d to rounded x.
func (r Rounder) Round(c *Context, d, x *Decimal) Condition {
d.Set(x)
Expand Down Expand Up @@ -78,7 +95,7 @@ func (r Rounder) Round(c *Context, d, x *Decimal) Condition {
var discard Decimal
discard.Coeff.Set(&m)
discard.Exponent = int32(-diff)
if r(&y, x.Negative, discard.Cmp(decimalHalf)) {
if r.ShouldAddOne(&y, x.Negative, discard.Cmp(decimalHalf)) {
roundAddOne(&y, &diff)
}
}
Expand All @@ -104,44 +121,40 @@ func roundAddOne(b *BigInt, diff *int64) {
}
}

var (
// Roundings defines the set of Rounders used by Context. Users may add their
// own, but modification of this map is not safe during any other parallel
// Context operations.
Roundings = map[string]Rounder{
RoundDown: roundDown,
RoundHalfUp: roundHalfUp,
RoundHalfEven: roundHalfEven,
RoundCeiling: roundCeiling,
RoundFloor: roundFloor,
RoundHalfDown: roundHalfDown,
RoundUp: roundUp,
Round05Up: round05Up,
}
)
// roundings is a set containing all available Rounders.
var roundings = map[Rounder]struct{}{
RoundDown: {},
RoundHalfUp: {},
RoundHalfEven: {},
RoundCeiling: {},
RoundFloor: {},
RoundHalfDown: {},
RoundUp: {},
Round05Up: {},
}

const (
// RoundDown rounds toward 0; truncate.
RoundDown = "down"
RoundDown Rounder = "down"
// RoundHalfUp rounds up if the digits are >= 0.5.
RoundHalfUp = "half_up"
RoundHalfUp Rounder = "half_up"
// RoundHalfEven rounds up if the digits are > 0.5. If the digits are equal
// to 0.5, it rounds up if the previous digit is odd, always producing an
// even digit.
RoundHalfEven = "half_even"
RoundHalfEven Rounder = "half_even"
// RoundCeiling towards +Inf: rounds up if digits are > 0 and the number
// is positive.
RoundCeiling = "ceiling"
RoundCeiling Rounder = "ceiling"
// RoundFloor towards -Inf: rounds up if digits are > 0 and the number
// is negative.
RoundFloor = "floor"
RoundFloor Rounder = "floor"
// RoundHalfDown rounds up if the digits are > 0.5.
RoundHalfDown = "half_down"
RoundHalfDown Rounder = "half_down"
// RoundUp rounds away from 0.
RoundUp = "up"
RoundUp Rounder = "up"
// Round05Up rounds zero or five away from 0; same as round-up, except that
// rounding up only occurs if the digit to be rounded up is 0 or 5.
Round05Up = "05up"
Round05Up Rounder = "05up"
)

func roundDown(result *BigInt, neg bool, half int) bool {
Expand Down

0 comments on commit c951ca9

Please sign in to comment.