VictoriaMetrics/lib/decimal/decimal.go
Aliaksandr Valialkin 75eeea21ee lib/decimal: reduce rounding error when converting from decimal to float with negative exponent
While at it, slightly increase the conversion performance by moving fast path to the top of the loop.
2019-11-19 23:35:33 +02:00

415 lines
8.2 KiB
Go

package decimal
import (
"math"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fastnum"
)
// CalibrateScale calibrates a and b with the corresponding exponents ae, be
// and returns the resulting exponent e.
func CalibrateScale(a []int64, ae int16, b []int64, be int16) (e int16) {
if ae == be {
// Fast path - exponents are equal.
return ae
}
if len(a) == 0 {
return be
}
if len(b) == 0 {
return ae
}
if ae < be {
a, b = b, a
ae, be = be, ae
}
upExp := ae - be
downExp := int16(0)
for _, v := range a {
maxUpExp := maxUpExponent(v)
if upExp-maxUpExp > downExp {
downExp = upExp - maxUpExp
}
}
upExp -= downExp
for i, v := range a {
adjExp := upExp
for adjExp > 0 {
v *= 10
adjExp--
}
a[i] = v
}
if downExp > 0 {
for i, v := range b {
if v == vInfPos || v == vInfNeg {
// Special case for these values - do not touch them.
continue
}
adjExp := downExp
for adjExp > 0 {
v /= 10
adjExp--
}
b[i] = v
}
}
return be + downExp
}
// ExtendFloat64sCapacity extends dst capacity to hold additionalItems
// and returns the extended dst.
func ExtendFloat64sCapacity(dst []float64, additionalItems int) []float64 {
dstLen := len(dst)
if n := dstLen + additionalItems - cap(dst); n > 0 {
dst = append(dst[:cap(dst)], make([]float64, n)...)
}
return dst[:dstLen]
}
// ExtendInt64sCapacity extends dst capacity to hold additionalItems
// and returns the extended dst.
func ExtendInt64sCapacity(dst []int64, additionalItems int) []int64 {
dstLen := len(dst)
if n := dstLen + additionalItems - cap(dst); n > 0 {
dst = append(dst[:cap(dst)], make([]int64, n)...)
}
return dst[:dstLen]
}
// AppendDecimalToFloat converts each item in va to f=v*10^e, appends it
// to dst and returns the resulting dst.
func AppendDecimalToFloat(dst []float64, va []int64, e int16) []float64 {
if fastnum.IsInt64Zeros(va) {
return fastnum.AppendFloat64Zeros(dst, len(va))
}
if e == 0 && fastnum.IsInt64Ones(va) {
return fastnum.AppendFloat64Ones(dst, len(va))
}
// Extend dst capacity in order to eliminate memory allocations below.
dst = ExtendFloat64sCapacity(dst, len(va))
// increase conversion precision for negative exponents by dividing by e10
isMul := true
if e < 0 {
e = -e
isMul = false
}
e10 := math.Pow10(int(e))
for _, v := range va {
// Manually inline ToFloat here for better performance
f := float64(v)
if isMul {
f *= e10
} else {
f /= e10
}
if v == vInfPos {
f = infPos
} else if v == vInfNeg {
f = infNeg
}
dst = append(dst, f)
}
return dst
}
// AppendFloatToDecimal converts each item in src to v*10^e and appends
// each v to dst returning it as va.
//
// It tries minimizing each item in dst.
func AppendFloatToDecimal(dst []int64, src []float64) (va []int64, e int16) {
if len(src) == 0 {
return dst, 0
}
if fastnum.IsFloat64Zeros(src) {
dst = fastnum.AppendInt64Zeros(dst, len(src))
return dst, 0
}
if fastnum.IsFloat64Ones(src) {
dst = fastnum.AppendInt64Ones(dst, len(src))
return dst, 0
}
// Extend dst capacity in order to eliminate memory allocations below.
dst = ExtendInt64sCapacity(dst, len(src))
vaev := vaeBufPool.Get()
if vaev == nil {
vaev = &vaeBuf{
va: make([]int64, len(src)),
ea: make([]int16, len(src)),
}
}
vae := vaev.(*vaeBuf)
vae.va = vae.va[:0]
vae.ea = vae.ea[:0]
// Determine the minimum exponent across all src items.
v, exp := FromFloat(src[0])
vae.va = append(vae.va, v)
vae.ea = append(vae.ea, exp)
minExp := exp
for _, f := range src[1:] {
v, exp := FromFloat(f)
vae.va = append(vae.va, v)
vae.ea = append(vae.ea, exp)
if exp < minExp {
minExp = exp
}
}
// Determine whether all the src items may be upscaled to minExp.
// If not, adjust minExp accordingly.
downExp := int16(0)
for i, v := range vae.va {
exp := vae.ea[i]
upExp := exp - minExp
maxUpExp := maxUpExponent(v)
if upExp-maxUpExp > downExp {
downExp = upExp - maxUpExp
}
}
minExp += downExp
// Scale each item in src to minExp and append it to dst.
for i, v := range vae.va {
exp := vae.ea[i]
adjExp := exp - minExp
for adjExp > 0 {
v *= 10
adjExp--
}
for adjExp < 0 {
v /= 10
adjExp++
}
dst = append(dst, v)
}
vaeBufPool.Put(vae)
return dst, minExp
}
type vaeBuf struct {
va []int64
ea []int16
}
var vaeBufPool sync.Pool
func maxUpExponent(v int64) int16 {
if v == 0 {
// Any exponent allowed.
return 1024
}
if v < 0 {
v = -v
}
if v < 0 {
// Handle corner case for v=-1<<63
return 0
}
maxMultiplier := ((1 << 63) - 1) / uint64(v)
switch {
case maxMultiplier >= 1e19:
return 19
case maxMultiplier >= 1e18:
return 18
case maxMultiplier >= 1e17:
return 17
case maxMultiplier >= 1e16:
return 16
case maxMultiplier >= 1e15:
return 15
case maxMultiplier >= 1e14:
return 14
case maxMultiplier >= 1e13:
return 13
case maxMultiplier >= 1e12:
return 12
case maxMultiplier >= 1e11:
return 11
case maxMultiplier >= 1e10:
return 10
case maxMultiplier >= 1e9:
return 9
case maxMultiplier >= 1e8:
return 8
case maxMultiplier >= 1e7:
return 7
case maxMultiplier >= 1e6:
return 6
case maxMultiplier >= 1e5:
return 5
case maxMultiplier >= 1e4:
return 4
case maxMultiplier >= 1e3:
return 3
case maxMultiplier >= 1e2:
return 2
case maxMultiplier >= 1e1:
return 1
default:
return 0
}
}
// ToFloat returns f=v*10^e.
func ToFloat(v int64, e int16) float64 {
// increase conversion precision for negative exponents by dividing by e10
isMul := true
if e < 0 {
e = -e
isMul = false
}
e10 := math.Pow10(int(e))
f := float64(v)
if isMul {
f *= e10
} else {
f /= e10
}
if v == vInfPos {
return infPos
}
if v == vInfNeg {
return infNeg
}
return f
}
const (
vInfPos = 1<<63 - 1
vInfNeg = -1 << 63
vMax = 1<<63 - 3
vMin = -1<<63 + 1
)
var (
infPos = math.Inf(1)
infNeg = math.Inf(-1)
)
// FromFloat converts f to v*10^e.
//
// It tries minimizing v.
// For instance, for f = -1.234 it returns v = -1234, e = -3.
//
// FromFloat doesn't work properly with NaN values, so don't pass them here.
func FromFloat(f float64) (int64, int16) {
if f == 0 {
return 0, 0
}
if math.IsInf(f, 0) {
return fromFloatInf(f)
}
if f > 0 {
v, e := positiveFloatToDecimal(f)
if v > vMax {
v = vMax
}
return v, e
}
v, e := positiveFloatToDecimal(-f)
v = -v
if v < vMin {
v = vMin
}
return v, e
}
func fromFloatInf(f float64) (int64, int16) {
// Special case for Inf
if math.IsInf(f, 1) {
return vInfPos, 0
}
return vInfNeg, 0
}
func positiveFloatToDecimal(f float64) (int64, int16) {
// There is no need in checking for f == 0, since it should be already checked by the caller.
u := uint64(f)
if float64(u) != f {
return positiveFloatToDecimalSlow(f)
}
// Fast path for integers.
if u < 1<<55 && u%10 != 0 {
return int64(u), 0
}
return getDecimalAndScale(u)
}
func getDecimalAndScale(u uint64) (int64, int16) {
var scale int16
for u >= 1<<55 {
// Remove trailing garbage bits left after float64->uint64 conversion,
// since float64 contains only 53 significant bits.
// See https://en.wikipedia.org/wiki/Double-precision_floating-point_format
u /= 10
scale++
}
if u%10 != 0 {
return int64(u), scale
}
// Minimize v by converting trailing zeros to scale.
u /= 10
scale++
for u != 0 && u%10 == 0 {
u /= 10
scale++
}
return int64(u), scale
}
func positiveFloatToDecimalSlow(f float64) (int64, int16) {
// Slow path for floating point numbers.
var scale int16
prec := conversionPrecision
if f > 1e6 || f < 1e-6 {
// Normalize f, so it is in the small range suitable
// for the next loop.
if f > 1e6 {
// Increase conversion precision for big numbers.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/213
prec = 1e15
}
_, exp := math.Frexp(f)
scale = int16(float64(exp) * math.Ln2 / math.Ln10)
f *= math.Pow10(-int(scale))
}
// Multiply f by 100 until the fractional part becomes
// too small comparing to integer part.
for f < prec {
x, frac := math.Modf(f)
if frac*prec < x {
f = x
break
}
if (1-frac)*prec < x {
f = x + 1
break
}
f *= 100
scale -= 2
}
u := uint64(f)
if u%10 != 0 {
return int64(u), scale
}
// Minimize u by converting trailing zero to scale.
u /= 10
scale++
return int64(u), scale
}
const conversionPrecision = 1e12