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

[DNM] apd: embed small coefficient values in Decimal struct #101

Closed
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
5 changes: 5 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (c *Context) add(d, x, y *Decimal, subtract bool) (Condition, error) {
if err != nil {
return 0, errors.Wrap(err, "add")
}
d.lazyInit()
d.Negative = xn
if xn == yn {
d.Coeff.Add(a, b)
Expand Down Expand Up @@ -203,6 +204,7 @@ func (c *Context) Mul(d, x, y *Decimal) (Condition, error) {
return 0, nil
}

d.lazyInit()
d.Coeff.Mul(&x.Coeff, &y.Coeff)
d.Negative = neg
d.Form = Finite
Expand Down Expand Up @@ -284,6 +286,7 @@ func (c *Context) Quo(d, x, y *Decimal) (Condition, error) {
var adjust int64
// The result coefficient is initialized to 0.
quo := new(Decimal)
quo.lazyInit()
var res Condition
var diff int64
if !x.IsZero() {
Expand Down Expand Up @@ -379,6 +382,7 @@ func (c *Context) QuoInteger(d, x, y *Decimal) (Condition, error) {
if err != nil {
return 0, errors.Wrap(err, "QuoInteger")
}
d.lazyInit()
d.Coeff.Quo(a, b)
d.Form = Finite
if d.NumDigits() > int64(c.Precision) {
Expand Down Expand Up @@ -420,6 +424,7 @@ func (c *Context) Rem(d, x, y *Decimal) (Condition, error) {
if err != nil {
return 0, errors.Wrap(err, "Rem")
}
d.lazyInit()
tmp := new(big.Int)
tmp.QuoRem(a, b, &d.Coeff)
if NumDigits(tmp) > int64(c.Precision) {
Expand Down
76 changes: 54 additions & 22 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"math/big"
"strconv"
"strings"
"unsafe"

"github.com/pkg/errors"
)
Expand All @@ -31,14 +32,15 @@ import (
// Coeff must be positive. If it is negative results may be incorrect and
// apd may panic.
type Decimal struct {
Form Form
Negative bool
Exponent int32
Coeff big.Int
Form Form
Negative bool
Exponent int32
Coeff big.Int
coeffInline [inlineCoeffCap]big.Word
}

// Form specifies the form of a Decimal.
type Form int
type Form int8

const (
// These constants must be in the following order. CmpTotal assumes that
Expand All @@ -55,6 +57,17 @@ const (
NaN
)

// Each Decimal maintains (through big.Int) an internal reference to a
// variable-length coefficient, which is represented by a []big.Word. The
// coeffInline field and the lazyInit method combine to allow us to inline this
// coefficient within the Decimal struct when its value is sufficiently small.
// In lazyInit, we point the Coeff field's backing slice at the coeffInline
// array. big.Int will avoid re-allocating this array until it is provided with
// a value that exceeds the initial capacity. We set this initial capacity to
// accommodate any coefficient that would fit in a 64-bit integer (i.e. values
// up to 2^64).
const inlineCoeffCap = unsafe.Sizeof(uint64(0)) / unsafe.Sizeof(big.Word(0))

var (
decimalNaN = &Decimal{Form: NaN}
decimalInfinity = &Decimal{Form: Infinite}
Expand All @@ -78,28 +91,34 @@ const (

// New creates a new decimal with the given coefficient and exponent.
func New(coeff int64, exponent int32) *Decimal {
d := &Decimal{
Negative: coeff < 0,
Coeff: *big.NewInt(coeff),
Exponent: exponent,
}
d.Coeff.Abs(&d.Coeff)
d := new(Decimal)
d.SetFinite(coeff, exponent)
return d
}

// NewWithBigInt creates a new decimal with the given coefficient and exponent.
func NewWithBigInt(coeff *big.Int, exponent int32) *Decimal {
d := &Decimal{
Exponent: exponent,
}
d := new(Decimal)
d.lazyInit()
d.Coeff.Set(coeff)
if d.Coeff.Sign() < 0 {
d.Negative = true
d.Coeff.Abs(&d.Coeff)
}
d.Exponent = exponent
return d
}

// lazyInit lazily initializes a zero Decimal value. Use of the method is not
// required for correctness, but can improve performance by avoiding a separate
// heap allocation within the Coeff field.
func (d *Decimal) lazyInit() {
if d.Coeff.Bits() == nil {
d.coeffInline = [inlineCoeffCap]big.Word{} // zero, to be safe
d.Coeff.SetBits(d.coeffInline[:0])
}
}

func consumePrefix(s, prefix string) (string, bool) {
if strings.HasPrefix(s, prefix) {
return s[len(prefix):], true
Expand All @@ -114,6 +133,7 @@ func (d *Decimal) setString(c *Context, s string) (Condition, error) {
s, _ = consumePrefix(s, "+")
}
s = strings.ToLower(s)
d.lazyInit()
d.Exponent = 0
d.Coeff.SetInt64(0)
// Until there are no parse errors, leave as NaN.
Expand Down Expand Up @@ -207,6 +227,7 @@ func (d *Decimal) Set(x *Decimal) *Decimal {
if d == x {
return d
}
d.lazyInit()
d.Negative = x.Negative
d.Coeff.Set(&x.Coeff)
d.Exponent = x.Exponent
Expand All @@ -230,6 +251,7 @@ func (d *Decimal) SetFinite(x int64, e int32) *Decimal {
// to Finite The exponent is not changed. Since the exponent is not changed
// (and this is thus easy to misuse), this is unexported for internal use only.
func (d *Decimal) setCoefficient(x int64) {
d.lazyInit()
d.Negative = x < 0
d.Coeff.SetInt64(x)
d.Coeff.Abs(&d.Coeff)
Expand All @@ -243,7 +265,17 @@ func (d *Decimal) SetFloat64(f float64) (*Decimal, error) {
return d, err
}

// Int64 returns the int64 representation of x. If x cannot be represented in an int64, an error is returned.
// CoeffRef returns a reference to d's Coefficient. This can be used by clients
// who want direct mutable access to a Decimal's coefficient field.
//
// WARNING: Use with care. The coefficient must not be set to a negative value.
func (d *Decimal) CoeffRef() *big.Int {
d.lazyInit()
return &d.Coeff
}

// Int64 returns the int64 representation of x. If x cannot be represented in an
// int64, an error is returned.
func (d *Decimal) Int64() (int64, error) {
if d.Form != Finite {
return 0, errors.Errorf("%s is not finite", d)
Expand Down Expand Up @@ -325,11 +357,7 @@ func (d *Decimal) setExponent(c *Context, res Condition, xs ...int64) Condition
// fractional parts and do operations similar Round. We avoid calling Round
// directly because it calls setExponent and modifies the result's exponent
// and coeff in ways that would be wrong here.
b := new(big.Int).Set(&d.Coeff)
tmp := &Decimal{
Coeff: *b,
Exponent: r - Etiny,
}
tmp := NewWithBigInt(&d.Coeff, r-Etiny)
integ, frac := new(Decimal), new(Decimal)
tmp.Modf(integ, frac)
frac.Abs(frac)
Expand Down Expand Up @@ -622,6 +650,7 @@ func (d *Decimal) Modf(integ, frac *Decimal) {
// No fractional part.
if d.Exponent > 0 {
if frac != nil {
frac.lazyInit()
frac.Negative = neg
frac.Exponent = 0
frac.Coeff.SetInt64(0)
Expand All @@ -636,6 +665,7 @@ func (d *Decimal) Modf(integ, frac *Decimal) {
// d < 0 because exponent is larger than number of digits.
if exp > nd {
if integ != nil {
integ.lazyInit()
integ.Negative = neg
integ.Exponent = 0
integ.Coeff.SetInt64(0)
Expand All @@ -650,19 +680,21 @@ func (d *Decimal) Modf(integ, frac *Decimal) {

var icoeff *big.Int
if integ != nil {
icoeff = &integ.Coeff
integ.lazyInit()
integ.Exponent = 0
integ.Negative = neg
icoeff = &integ.Coeff
} else {
// This is the integ == nil branch, and we already checked if both integ and
// frac were nil above, so frac can never be nil in this branch.
icoeff = new(big.Int)
}

if frac != nil {
icoeff.QuoRem(&d.Coeff, e, &frac.Coeff)
frac.lazyInit()
frac.Exponent = d.Exponent
frac.Negative = neg
icoeff.QuoRem(&d.Coeff, e, &frac.Coeff)
} else {
// This is the frac == nil, which means integ must not be nil since they both
// can't be due to the check above.
Expand Down
1 change: 1 addition & 0 deletions decomposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func (d *Decimal) Compose(form byte, negative bool, coefficient []byte, exponent
return nil
}
// Finite form.
d.lazyInit()
d.Negative = negative
d.Coeff.SetBytes(coefficient)
d.Exponent = exponent
Expand Down
57 changes: 34 additions & 23 deletions gda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,32 +296,43 @@ func (tc TestCase) Run(c *Context, done chan error, d, x, y *Decimal) (res Condi
func BenchmarkGDA(b *testing.B) {
for _, fname := range GDAfiles {
b.Run(fname, func(b *testing.B) {
b.StopTimer()
type benchCase struct {
tc TestCase
ctx *Context
res []Decimal
ops [2]*Decimal
}
_, tcs := readGDA(b, fname)
res := new(Decimal)
for i := 0; i < b.N; i++ {
Loop:
for _, tc := range tcs {
if GDAignore[tc.ID] || tc.Result == "?" || tc.HasNull() {
continue
}
switch tc.Operation {
case "apply", "toeng":
continue
}
operands := make([]*Decimal, 2)
for i, o := range tc.Operands {
d, _, err := NewFromString(o)
if err != nil {
continue Loop
}
operands[i] = d
bcs := make([]benchCase, 0, len(tcs))
Loop:
for _, tc := range tcs {
if GDAignore[tc.ID] || tc.Result == "?" || tc.HasNull() {
continue
}
switch tc.Operation {
case "apply", "toeng":
continue
}
bc := benchCase{
tc: tc,
ctx: tc.Context(b),
res: make([]Decimal, b.N),
}
for i, o := range tc.Operands {
d, _, err := NewFromString(o)
if err != nil {
continue Loop
}
c := tc.Context(b)
b.StartTimer()
bc.ops[i] = d
}
bcs = append(bcs, bc)
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, bc := range bcs {
// Ignore errors here because the full tests catch them.
_, _ = tc.Run(c, nil, res, operands[0], operands[1])
b.StopTimer()
_, _ = bc.tc.Run(bc.ctx, nil, &bc.res[i], bc.ops[0], bc.ops[1])
}
}
})
Expand Down
46 changes: 26 additions & 20 deletions table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,50 @@ import (
)

func BenchmarkNumDigitsLookup(b *testing.B) {
b.StopTimer()
runTest := func(start string, c byte) {
prep := func(start string, c byte) []*Decimal {
var ds []*Decimal
buf := bytes.NewBufferString(start)
for i := 1; i < digitsTableSize; i++ {
buf.WriteByte(c)
d, _, _ := NewFromString(buf.String())

b.StartTimer()
d.NumDigits()
b.StopTimer()
ds = append(ds, d)
}
return ds
}
var ds []*Decimal
ds = append(ds, prep("", '9')...)
ds = append(ds, prep("1", '0')...)
ds = append(ds, prep("-", '9')...)
ds = append(ds, prep("-1", '0')...)
b.ResetTimer()
for i := 0; i < b.N; i++ {
runTest("", '9')
runTest("1", '0')
runTest("-", '9')
runTest("-1", '0')
for _, d := range ds {
d.NumDigits()
}
}
}

func BenchmarkNumDigitsFull(b *testing.B) {
b.StopTimer()
runTest := func(start string, c byte) {
prep := func(start string, c byte) []*Decimal {
var ds []*Decimal
buf := bytes.NewBufferString(start)
for i := 1; i < 1000; i++ {
buf.WriteByte(c)
d, _, _ := NewFromString(buf.String())

b.StartTimer()
d.NumDigits()
b.StopTimer()
ds = append(ds, d)
}
return ds
}
var ds []*Decimal
ds = append(ds, prep("", '9')...)
ds = append(ds, prep("1", '0')...)
ds = append(ds, prep("-", '9')...)
ds = append(ds, prep("-1", '0')...)
b.ResetTimer()
for i := 0; i < b.N; i++ {
runTest("", '9')
runTest("1", '0')
runTest("-", '9')
runTest("-1", '0')
for _, d := range ds {
d.NumDigits()
}
}
}

Expand Down