From 595dc5155bed2253d1578d48bc5b4a4184ef9434 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Sat, 14 Sep 2024 15:21:58 -0700 Subject: [PATCH] Make accurate Quantity->float64 be a new method This leaves the old method alone, since the performance difference is so stark. ``` $ go test ./staging/src/k8s.io/apimachinery/pkg/api/resource/ -bench=Float64 goos: linux goarch: amd64 pkg: k8s.io/apimachinery/pkg/api/resource cpu: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz BenchmarkQuantityAsApproximateFloat64-6 95865672 11.44 ns/op BenchmarkQuantityAsFloat64Slow-6 2800825 430.2 ns/op PASS ok k8s.io/apimachinery/pkg/api/resource 2.786s ``` --- .../apimachinery/pkg/api/resource/quantity.go | 29 ++++- .../pkg/api/resource/quantity_test.go | 122 +++++++++++++++++- 2 files changed, 146 insertions(+), 5 deletions(-) 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 5a6bd1c4414..d0aada9dd75 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" "math/big" "strconv" "strings" @@ -459,10 +460,32 @@ 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 + 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 + } + + 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 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 a6cfc7a809b..79d7f30b92a 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 @@ -1294,6 +1294,78 @@ 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 + }{ + {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) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way + {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) * 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 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("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.AsApproximateFloat64() + if out != item.out { + t.Fatalf("as int quantity: expected %v, got %v", item.out, out) + } + } + } + }) + } +} + +func TestQuantityAsFloat64Slow(t *testing.T) { + // NOTE: this table should be kept in sync with TestQuantityAsApproximateFloat64 table := []struct { in Quantity out float64 @@ -1346,14 +1418,14 @@ func TestQuantityAsApproximateFloat64(t *testing.T) { 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() + 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.AsApproximateFloat64() + out := q.AsFloat64Slow() if out != item.out { t.Fatalf("as int quantity: expected %v, got %v", item.out, out) } @@ -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), @@ -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) {