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
```
This commit is contained in:
Tim Hockin 2024-09-14 15:21:58 -07:00
parent 75ba5a9651
commit 595dc5155b
No known key found for this signature in database
2 changed files with 146 additions and 5 deletions

View File

@ -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

View File

@ -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) {