Skip to content

Commit

Permalink
Merge pull request #127366 from thockin/pr-119910-plus-thockin
Browse files Browse the repository at this point in the history
 Improve precision of Quantity export as float64

Kubernetes-commit: 8adc35ebd7221683b9f9ec7a9e2c63cf3f6d0dff
  • Loading branch information
k8s-publishing-bot committed Sep 19, 2024
2 parents c7c91ed + d1fb2f4 commit f7615f3
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 6 deletions.
39 changes: 35 additions & 4 deletions pkg/api/resource/quantity.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"bytes"
"errors"
"fmt"
"math"
math "math"
"math/big"
"strconv"
"strings"
Expand Down Expand Up @@ -460,9 +460,10 @@ func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) {
}
}

// AsApproximateFloat64 returns a float64 representation of the quantity which may
// lose precision. If the value of the quantity is outside the range of a float64
// +Inf/-Inf will be returned.
// AsApproximateFloat64 returns a float64 representation of the quantity which
// may lose precision. If precision matter more than performance, see
// AsFloat64Slow. If the value of the quantity is outside the range of a
// float64 +Inf/-Inf will be returned.
func (q *Quantity) AsApproximateFloat64() float64 {
var base float64
var exponent int
Expand All @@ -480,6 +481,36 @@ func (q *Quantity) AsApproximateFloat64() float64 {
return base * math.Pow10(exponent)
}

// AsFloat64Slow returns a float64 representation of the quantity. This is
// more precise than AsApproximateFloat64 but significantly slower. If the
// value of the quantity is outside the range of a float64 +Inf/-Inf will be
// returned.
func (q *Quantity) AsFloat64Slow() float64 {
infDec := q.AsDec()

var absScale int64
if infDec.Scale() < 0 {
absScale = int64(-infDec.Scale())
} else {
absScale = int64(infDec.Scale())
}
pow10AbsScale := big.NewInt(10)
pow10AbsScale = pow10AbsScale.Exp(pow10AbsScale, big.NewInt(absScale), nil)

var resultBigFloat *big.Float
if infDec.Scale() < 0 {
resultBigInt := new(big.Int).Mul(infDec.UnscaledBig(), pow10AbsScale)
resultBigFloat = new(big.Float).SetInt(resultBigInt)
} else {
pow10AbsScaleFloat := new(big.Float).SetInt(pow10AbsScale)
resultBigFloat = new(big.Float).SetInt(infDec.UnscaledBig())
resultBigFloat = resultBigFloat.Quo(resultBigFloat, pow10AbsScaleFloat)
}

result, _ := resultBigFloat.Float64()
return result
}

// AsInt64 returns a representation of the current value as an int64 if a fast conversion
// is possible. If false is returned, callers must use the inf.Dec form of this quantity.
func (q *Quantity) AsInt64() (int64, bool) {
Expand Down
122 changes: 120 additions & 2 deletions pkg/api/resource/quantity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,7 @@ func TestNegateRoundTrip(t *testing.T) {
}

func TestQuantityAsApproximateFloat64(t *testing.T) {
// NOTE: this table should be kept in sync with TestQuantityAsFloat64Slow
table := []struct {
in Quantity
out float64
Expand Down Expand Up @@ -1344,11 +1345,11 @@ func TestQuantityAsApproximateFloat64(t *testing.T) {
{decQuantity(-12, 500, DecimalSI), math.Inf(-1)},
}

for _, item := range table {
for i, item := range table {
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
out := item.in.AsApproximateFloat64()
if out != item.out {
t.Fatalf("expected %v, got %v", item.out, out)
t.Fatalf("test %d expected %v, got %v", i+1, item.out, out)
}
if item.in.d.Dec != nil {
if i, ok := item.in.AsInt64(); ok {
Expand All @@ -1363,6 +1364,77 @@ func TestQuantityAsApproximateFloat64(t *testing.T) {
}
}

func TestQuantityAsFloat64Slow(t *testing.T) {
// NOTE: this table should be kept in sync with TestQuantityAsApproximateFloat64
table := []struct {
in Quantity
out float64
}{
{decQuantity(0, 0, DecimalSI), 0.0},
{decQuantity(0, 0, DecimalExponent), 0.0},
{decQuantity(0, 0, BinarySI), 0.0},

{decQuantity(1, 0, DecimalSI), 1},
{decQuantity(1, 0, DecimalExponent), 1},
{decQuantity(1, 0, BinarySI), 1},

// Binary suffixes
{decQuantity(1024, 0, BinarySI), 1024},
{decQuantity(8*1024, 0, BinarySI), 8 * 1024},
{decQuantity(7*1024*1024, 0, BinarySI), 7 * 1024 * 1024},
{decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 10},
{decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * 10000},
{decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * 100000000},
{decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) / float64(10)},
{decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(100000000)},

{decQuantity(1024, 0, DecimalSI), 1024},
{decQuantity(8*1024, 0, DecimalSI), 8 * 1024},
{decQuantity(7*1024*1024, 0, DecimalSI), 7 * 1024 * 1024},
{decQuantity(7*1024*1024, 1, DecimalSI), (7 * 1024 * 1024) * 10},
{decQuantity(7*1024*1024, 4, DecimalSI), (7 * 1024 * 1024) * 10000},
{decQuantity(7*1024*1024, 8, DecimalSI), (7 * 1024 * 1024) * 100000000},
{decQuantity(7*1024*1024, -1, DecimalSI), (7 * 1024 * 1024) / float64(10)},
{decQuantity(7*1024*1024, -8, DecimalSI), (7 * 1024 * 1024) / float64(100000000)},

{decQuantity(1024, 0, DecimalExponent), 1024},
{decQuantity(8*1024, 0, DecimalExponent), 8 * 1024},
{decQuantity(7*1024*1024, 0, DecimalExponent), 7 * 1024 * 1024},
{decQuantity(7*1024*1024, 1, DecimalExponent), (7 * 1024 * 1024) * 10},
{decQuantity(7*1024*1024, 4, DecimalExponent), (7 * 1024 * 1024) * 10000},
{decQuantity(7*1024*1024, 8, DecimalExponent), (7 * 1024 * 1024) * 100000000},
{decQuantity(7*1024*1024, -1, DecimalExponent), (7 * 1024 * 1024) / float64(10)},
{decQuantity(7*1024*1024, -8, DecimalExponent), (7 * 1024 * 1024) / float64(100000000)},

// very large numbers
{Quantity{d: maxAllowed, Format: DecimalSI}, math.MaxInt64},
{Quantity{d: maxAllowed, Format: BinarySI}, math.MaxInt64},
{decQuantity(12, 18, DecimalSI), 1.2e19},

// infinities caused due to float64 overflow
{decQuantity(12, 500, DecimalSI), math.Inf(0)},
{decQuantity(-12, 500, DecimalSI), math.Inf(-1)},
}

for i, item := range table {
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
out := item.in.AsFloat64Slow()
if out != item.out {
t.Fatalf("test %d expected %v, got %v", i+1, item.out, out)
}
if item.in.d.Dec != nil {
if i, ok := item.in.AsInt64(); ok {
q := intQuantity(i, 0, item.in.Format)
out := q.AsFloat64Slow()
if out != item.out {
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
}
}
}
})
}
}

func TestStringQuantityAsApproximateFloat64(t *testing.T) {
table := []struct {
in string
Expand Down Expand Up @@ -1397,6 +1469,40 @@ func TestStringQuantityAsApproximateFloat64(t *testing.T) {
}
}

func TestStringQuantityAsFloat64Slow(t *testing.T) {
table := []struct {
in string
out float64
}{
{"2Ki", 2048},
{"1.1Ki", 1126.4e+0},
{"1Mi", 1.048576e+06},
{"2Gi", 2.147483648e+09},
}

for _, item := range table {
t.Run(item.in, func(t *testing.T) {
in, err := ParseQuantity(item.in)
if err != nil {
t.Fatal(err)
}
out := in.AsFloat64Slow()
if out != item.out {
t.Fatalf("expected %v, got %v", item.out, out)
}
if in.d.Dec != nil {
if i, ok := in.AsInt64(); ok {
q := intQuantity(i, 0, in.Format)
out := q.AsFloat64Slow()
if out != item.out {
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
}
}
}
})
}
}

func benchmarkQuantities() []Quantity {
return []Quantity{
intQuantity(1024*1024*1024, 0, BinarySI),
Expand Down Expand Up @@ -1579,6 +1685,18 @@ func BenchmarkQuantityAsApproximateFloat64(b *testing.B) {
b.StopTimer()
}

func BenchmarkQuantityAsFloat64Slow(b *testing.B) {
values := benchmarkQuantities()
b.ResetTimer()
for i := 0; i < b.N; i++ {
q := values[i%len(values)]
if q.AsFloat64Slow() == -1 {
b.Fatal(q)
}
}
b.StopTimer()
}

var _ pflag.Value = &QuantityValue{}

func TestQuantityValueSet(t *testing.T) {
Expand Down

0 comments on commit f7615f3

Please sign in to comment.