Skip to content

Commit 8adc35e

Browse files
authored
Merge pull request kubernetes#127366 from thockin/pr-119910-plus-thockin
Improve precision of Quantity export as float64
2 parents dddffaa + 595dc51 commit 8adc35e

File tree

2 files changed

+155
-6
lines changed

2 files changed

+155
-6
lines changed

staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"bytes"
2121
"errors"
2222
"fmt"
23-
"math"
23+
math "math"
2424
"math/big"
2525
"strconv"
2626
"strings"
@@ -460,9 +460,10 @@ func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) {
460460
}
461461
}
462462

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

484+
// AsFloat64Slow returns a float64 representation of the quantity. This is
485+
// more precise than AsApproximateFloat64 but significantly slower. If the
486+
// value of the quantity is outside the range of a float64 +Inf/-Inf will be
487+
// returned.
488+
func (q *Quantity) AsFloat64Slow() float64 {
489+
infDec := q.AsDec()
490+
491+
var absScale int64
492+
if infDec.Scale() < 0 {
493+
absScale = int64(-infDec.Scale())
494+
} else {
495+
absScale = int64(infDec.Scale())
496+
}
497+
pow10AbsScale := big.NewInt(10)
498+
pow10AbsScale = pow10AbsScale.Exp(pow10AbsScale, big.NewInt(absScale), nil)
499+
500+
var resultBigFloat *big.Float
501+
if infDec.Scale() < 0 {
502+
resultBigInt := new(big.Int).Mul(infDec.UnscaledBig(), pow10AbsScale)
503+
resultBigFloat = new(big.Float).SetInt(resultBigInt)
504+
} else {
505+
pow10AbsScaleFloat := new(big.Float).SetInt(pow10AbsScale)
506+
resultBigFloat = new(big.Float).SetInt(infDec.UnscaledBig())
507+
resultBigFloat = resultBigFloat.Quo(resultBigFloat, pow10AbsScaleFloat)
508+
}
509+
510+
result, _ := resultBigFloat.Float64()
511+
return result
512+
}
513+
483514
// AsInt64 returns a representation of the current value as an int64 if a fast conversion
484515
// is possible. If false is returned, callers must use the inf.Dec form of this quantity.
485516
func (q *Quantity) AsInt64() (int64, bool) {

staging/src/k8s.io/apimachinery/pkg/api/resource/quantity_test.go

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,7 @@ func TestNegateRoundTrip(t *testing.T) {
12941294
}
12951295

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

1347-
for _, item := range table {
1348+
for i, item := range table {
13481349
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
13491350
out := item.in.AsApproximateFloat64()
13501351
if out != item.out {
1351-
t.Fatalf("expected %v, got %v", item.out, out)
1352+
t.Fatalf("test %d expected %v, got %v", i+1, item.out, out)
13521353
}
13531354
if item.in.d.Dec != nil {
13541355
if i, ok := item.in.AsInt64(); ok {
@@ -1363,6 +1364,77 @@ func TestQuantityAsApproximateFloat64(t *testing.T) {
13631364
}
13641365
}
13651366

1367+
func TestQuantityAsFloat64Slow(t *testing.T) {
1368+
// NOTE: this table should be kept in sync with TestQuantityAsApproximateFloat64
1369+
table := []struct {
1370+
in Quantity
1371+
out float64
1372+
}{
1373+
{decQuantity(0, 0, DecimalSI), 0.0},
1374+
{decQuantity(0, 0, DecimalExponent), 0.0},
1375+
{decQuantity(0, 0, BinarySI), 0.0},
1376+
1377+
{decQuantity(1, 0, DecimalSI), 1},
1378+
{decQuantity(1, 0, DecimalExponent), 1},
1379+
{decQuantity(1, 0, BinarySI), 1},
1380+
1381+
// Binary suffixes
1382+
{decQuantity(1024, 0, BinarySI), 1024},
1383+
{decQuantity(8*1024, 0, BinarySI), 8 * 1024},
1384+
{decQuantity(7*1024*1024, 0, BinarySI), 7 * 1024 * 1024},
1385+
{decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 10},
1386+
{decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * 10000},
1387+
{decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * 100000000},
1388+
{decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) / float64(10)},
1389+
{decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(100000000)},
1390+
1391+
{decQuantity(1024, 0, DecimalSI), 1024},
1392+
{decQuantity(8*1024, 0, DecimalSI), 8 * 1024},
1393+
{decQuantity(7*1024*1024, 0, DecimalSI), 7 * 1024 * 1024},
1394+
{decQuantity(7*1024*1024, 1, DecimalSI), (7 * 1024 * 1024) * 10},
1395+
{decQuantity(7*1024*1024, 4, DecimalSI), (7 * 1024 * 1024) * 10000},
1396+
{decQuantity(7*1024*1024, 8, DecimalSI), (7 * 1024 * 1024) * 100000000},
1397+
{decQuantity(7*1024*1024, -1, DecimalSI), (7 * 1024 * 1024) / float64(10)},
1398+
{decQuantity(7*1024*1024, -8, DecimalSI), (7 * 1024 * 1024) / float64(100000000)},
1399+
1400+
{decQuantity(1024, 0, DecimalExponent), 1024},
1401+
{decQuantity(8*1024, 0, DecimalExponent), 8 * 1024},
1402+
{decQuantity(7*1024*1024, 0, DecimalExponent), 7 * 1024 * 1024},
1403+
{decQuantity(7*1024*1024, 1, DecimalExponent), (7 * 1024 * 1024) * 10},
1404+
{decQuantity(7*1024*1024, 4, DecimalExponent), (7 * 1024 * 1024) * 10000},
1405+
{decQuantity(7*1024*1024, 8, DecimalExponent), (7 * 1024 * 1024) * 100000000},
1406+
{decQuantity(7*1024*1024, -1, DecimalExponent), (7 * 1024 * 1024) / float64(10)},
1407+
{decQuantity(7*1024*1024, -8, DecimalExponent), (7 * 1024 * 1024) / float64(100000000)},
1408+
1409+
// very large numbers
1410+
{Quantity{d: maxAllowed, Format: DecimalSI}, math.MaxInt64},
1411+
{Quantity{d: maxAllowed, Format: BinarySI}, math.MaxInt64},
1412+
{decQuantity(12, 18, DecimalSI), 1.2e19},
1413+
1414+
// infinities caused due to float64 overflow
1415+
{decQuantity(12, 500, DecimalSI), math.Inf(0)},
1416+
{decQuantity(-12, 500, DecimalSI), math.Inf(-1)},
1417+
}
1418+
1419+
for i, item := range table {
1420+
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
1421+
out := item.in.AsFloat64Slow()
1422+
if out != item.out {
1423+
t.Fatalf("test %d expected %v, got %v", i+1, item.out, out)
1424+
}
1425+
if item.in.d.Dec != nil {
1426+
if i, ok := item.in.AsInt64(); ok {
1427+
q := intQuantity(i, 0, item.in.Format)
1428+
out := q.AsFloat64Slow()
1429+
if out != item.out {
1430+
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
1431+
}
1432+
}
1433+
}
1434+
})
1435+
}
1436+
}
1437+
13661438
func TestStringQuantityAsApproximateFloat64(t *testing.T) {
13671439
table := []struct {
13681440
in string
@@ -1397,6 +1469,40 @@ func TestStringQuantityAsApproximateFloat64(t *testing.T) {
13971469
}
13981470
}
13991471

1472+
func TestStringQuantityAsFloat64Slow(t *testing.T) {
1473+
table := []struct {
1474+
in string
1475+
out float64
1476+
}{
1477+
{"2Ki", 2048},
1478+
{"1.1Ki", 1126.4e+0},
1479+
{"1Mi", 1.048576e+06},
1480+
{"2Gi", 2.147483648e+09},
1481+
}
1482+
1483+
for _, item := range table {
1484+
t.Run(item.in, func(t *testing.T) {
1485+
in, err := ParseQuantity(item.in)
1486+
if err != nil {
1487+
t.Fatal(err)
1488+
}
1489+
out := in.AsFloat64Slow()
1490+
if out != item.out {
1491+
t.Fatalf("expected %v, got %v", item.out, out)
1492+
}
1493+
if in.d.Dec != nil {
1494+
if i, ok := in.AsInt64(); ok {
1495+
q := intQuantity(i, 0, in.Format)
1496+
out := q.AsFloat64Slow()
1497+
if out != item.out {
1498+
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
1499+
}
1500+
}
1501+
}
1502+
})
1503+
}
1504+
}
1505+
14001506
func benchmarkQuantities() []Quantity {
14011507
return []Quantity{
14021508
intQuantity(1024*1024*1024, 0, BinarySI),
@@ -1579,6 +1685,18 @@ func BenchmarkQuantityAsApproximateFloat64(b *testing.B) {
15791685
b.StopTimer()
15801686
}
15811687

1688+
func BenchmarkQuantityAsFloat64Slow(b *testing.B) {
1689+
values := benchmarkQuantities()
1690+
b.ResetTimer()
1691+
for i := 0; i < b.N; i++ {
1692+
q := values[i%len(values)]
1693+
if q.AsFloat64Slow() == -1 {
1694+
b.Fatal(q)
1695+
}
1696+
}
1697+
b.StopTimer()
1698+
}
1699+
15821700
var _ pflag.Value = &QuantityValue{}
15831701

15841702
func TestQuantityValueSet(t *testing.T) {

0 commit comments

Comments
 (0)