Skip to content

Commit 595dc51

Browse files
committed
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 ```
1 parent 75ba5a9 commit 595dc51

File tree

2 files changed

+146
-5
lines changed

2 files changed

+146
-5
lines changed

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"errors"
2222
"fmt"
23+
math "math"
2324
"math/big"
2425
"strconv"
2526
"strings"
@@ -459,10 +460,32 @@ func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) {
459460
}
460461
}
461462

462-
// AsApproximateFloat64 returns a float64 representation of the quantity which may
463-
// lose precision. If the value of the quantity is outside the range of a float64
464-
// +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.
465467
func (q *Quantity) AsApproximateFloat64() float64 {
468+
var base float64
469+
var exponent int
470+
if q.d.Dec != nil {
471+
base, _ = big.NewFloat(0).SetInt(q.d.Dec.UnscaledBig()).Float64()
472+
exponent = int(-q.d.Dec.Scale())
473+
} else {
474+
base = float64(q.i.value)
475+
exponent = int(q.i.scale)
476+
}
477+
if exponent == 0 {
478+
return base
479+
}
480+
481+
return base * math.Pow10(exponent)
482+
}
483+
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 {
466489
infDec := q.AsDec()
467490

468491
var absScale int64

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,78 @@ func TestNegateRoundTrip(t *testing.T) {
12941294
}
12951295

12961296
func TestQuantityAsApproximateFloat64(t *testing.T) {
1297+
// NOTE: this table should be kept in sync with TestQuantityAsFloat64Slow
1298+
table := []struct {
1299+
in Quantity
1300+
out float64
1301+
}{
1302+
{decQuantity(0, 0, DecimalSI), 0.0},
1303+
{decQuantity(0, 0, DecimalExponent), 0.0},
1304+
{decQuantity(0, 0, BinarySI), 0.0},
1305+
1306+
{decQuantity(1, 0, DecimalSI), 1},
1307+
{decQuantity(1, 0, DecimalExponent), 1},
1308+
{decQuantity(1, 0, BinarySI), 1},
1309+
1310+
// Binary suffixes
1311+
{decQuantity(1024, 0, BinarySI), 1024},
1312+
{decQuantity(8*1024, 0, BinarySI), 8 * 1024},
1313+
{decQuantity(7*1024*1024, 0, BinarySI), 7 * 1024 * 1024},
1314+
{decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 10},
1315+
{decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * 10000},
1316+
{decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * 100000000},
1317+
{decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way
1318+
{decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(100000000)},
1319+
1320+
{decQuantity(1024, 0, DecimalSI), 1024},
1321+
{decQuantity(8*1024, 0, DecimalSI), 8 * 1024},
1322+
{decQuantity(7*1024*1024, 0, DecimalSI), 7 * 1024 * 1024},
1323+
{decQuantity(7*1024*1024, 1, DecimalSI), (7 * 1024 * 1024) * 10},
1324+
{decQuantity(7*1024*1024, 4, DecimalSI), (7 * 1024 * 1024) * 10000},
1325+
{decQuantity(7*1024*1024, 8, DecimalSI), (7 * 1024 * 1024) * 100000000},
1326+
{decQuantity(7*1024*1024, -1, DecimalSI), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way
1327+
{decQuantity(7*1024*1024, -8, DecimalSI), (7 * 1024 * 1024) / float64(100000000)},
1328+
1329+
{decQuantity(1024, 0, DecimalExponent), 1024},
1330+
{decQuantity(8*1024, 0, DecimalExponent), 8 * 1024},
1331+
{decQuantity(7*1024*1024, 0, DecimalExponent), 7 * 1024 * 1024},
1332+
{decQuantity(7*1024*1024, 1, DecimalExponent), (7 * 1024 * 1024) * 10},
1333+
{decQuantity(7*1024*1024, 4, DecimalExponent), (7 * 1024 * 1024) * 10000},
1334+
{decQuantity(7*1024*1024, 8, DecimalExponent), (7 * 1024 * 1024) * 100000000},
1335+
{decQuantity(7*1024*1024, -1, DecimalExponent), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way
1336+
{decQuantity(7*1024*1024, -8, DecimalExponent), (7 * 1024 * 1024) / float64(100000000)},
1337+
1338+
// very large numbers
1339+
{Quantity{d: maxAllowed, Format: DecimalSI}, math.MaxInt64},
1340+
{Quantity{d: maxAllowed, Format: BinarySI}, math.MaxInt64},
1341+
{decQuantity(12, 18, DecimalSI), 1.2e19},
1342+
1343+
// infinities caused due to float64 overflow
1344+
{decQuantity(12, 500, DecimalSI), math.Inf(0)},
1345+
{decQuantity(-12, 500, DecimalSI), math.Inf(-1)},
1346+
}
1347+
1348+
for i, item := range table {
1349+
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
1350+
out := item.in.AsApproximateFloat64()
1351+
if out != item.out {
1352+
t.Fatalf("test %d expected %v, got %v", i+1, item.out, out)
1353+
}
1354+
if item.in.d.Dec != nil {
1355+
if i, ok := item.in.AsInt64(); ok {
1356+
q := intQuantity(i, 0, item.in.Format)
1357+
out := q.AsApproximateFloat64()
1358+
if out != item.out {
1359+
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
1360+
}
1361+
}
1362+
}
1363+
})
1364+
}
1365+
}
1366+
1367+
func TestQuantityAsFloat64Slow(t *testing.T) {
1368+
// NOTE: this table should be kept in sync with TestQuantityAsApproximateFloat64
12971369
table := []struct {
12981370
in Quantity
12991371
out float64
@@ -1346,14 +1418,14 @@ func TestQuantityAsApproximateFloat64(t *testing.T) {
13461418

13471419
for i, item := range table {
13481420
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
1349-
out := item.in.AsApproximateFloat64()
1421+
out := item.in.AsFloat64Slow()
13501422
if out != item.out {
13511423
t.Fatalf("test %d expected %v, got %v", i+1, item.out, out)
13521424
}
13531425
if item.in.d.Dec != nil {
13541426
if i, ok := item.in.AsInt64(); ok {
13551427
q := intQuantity(i, 0, item.in.Format)
1356-
out := q.AsApproximateFloat64()
1428+
out := q.AsFloat64Slow()
13571429
if out != item.out {
13581430
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
13591431
}
@@ -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)