From c06c1121d1c07f72cf0d35c76a1637814efe9144 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 22 Sep 2020 17:16:20 -0400 Subject: [PATCH] quantity: Allow quantity to be converted to float64 Allow a fast approximate conversion to float64 from quantity (which is an arbitrary precision integral value). Will return +Inf/-Inf if a float64 is not sufficient to represent the current value. --- .../apimachinery/pkg/api/resource/quantity.go | 31 +++++++ .../pkg/api/resource/quantity_test.go | 85 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go b/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go index e0d9c783c36..8d718945d06 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go @@ -20,6 +20,7 @@ import ( "bytes" "errors" "fmt" + "math" "math/big" "strconv" "strings" @@ -442,6 +443,36 @@ 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. +func (q *Quantity) AsApproximateFloat64() float64 { + var base float64 + var exponent int + if q.d.Dec != nil { + base, _ = big.NewFloat(0).SetInt(q.d.Dec.UnscaledBig()).Float64() + exponent = int(-q.d.Dec.Scale()) + } else { + base = float64(q.i.value) + exponent = int(q.i.scale) + } + if exponent == 0 { + return base + } + + // multiply by the appropriate exponential scale + switch q.Format { + case DecimalExponent, DecimalSI: + return base * math.Pow10(exponent) + default: + // fast path for exponents that can fit in 64 bits + if exponent > 0 && exponent < 7 { + return base * float64(int64(1)<<(exponent*10)) + } + return base * math.Pow(2, float64(exponent*10)) + } +} + // 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) { diff --git a/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity_test.go b/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity_test.go index 1401a83bbcf..3c341d056f8 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity_test.go @@ -18,6 +18,8 @@ package resource import ( "encoding/json" + "fmt" + "math" "math/rand" "strings" "testing" @@ -1177,6 +1179,77 @@ func TestNegateRoundTrip(t *testing.T) { } } } + +func TestQuantityAsApproximateFloat64(t *testing.T) { + 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) * 1024}, + {decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024)}, + {decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)}, + {decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) / float64(1024)}, + {decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(1024*1024*1024*1024*1024*1024*1024*1024)}, + + {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) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way + {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) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way + {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 _, 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) + } + if item.in.d.Dec != nil { + if i, ok := item.in.AsInt64(); ok { + q := intQuantity(i, 0, item.in.Format) + out := q.AsApproximateFloat64() + 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), @@ -1346,3 +1419,15 @@ func BenchmarkQuantityCmp(b *testing.B) { } b.StopTimer() } + +func BenchmarkQuantityAsApproximateFloat64(b *testing.B) { + values := benchmarkQuantities() + b.ResetTimer() + for i := 0; i < b.N; i++ { + q := values[i%len(values)] + if q.AsApproximateFloat64() == -1 { + b.Fatal(q) + } + } + b.StopTimer() +}