From cf82d6b0049bb3e9a332dd4c64a8443e738a7f73 Mon Sep 17 00:00:00 2001 From: Xiang Li Date: Thu, 3 Dec 2015 13:11:50 -0800 Subject: [PATCH] resource: optimize scale function The original scale function takes around 800ns/op with more than 10 allocations. It significantly slow down scheduler and other components that heavily relys on resource pkg. For more information see #18126. This pull request tries to optimize scale function. It takes two approach: 1. when the value is small, only use normal math ops. 2. when the value is large, use math.Big with buffer pool. The final result is: BenchmarkScaledValueSmall-4 20000000 66.9 ns/op 0 B/op 0 allocs/op BenchmarkScaledValueLarge-4 2000000 711 ns/op 48 B/op 1 allocs/op I also run the scheduler benchmark again. It doubles the throughput of scheduler for 1000 nodes case. --- pkg/api/resource/quantity.go | 6 +- pkg/api/resource/scale_int.go | 95 ++++++++++++++++++++++++++++++ pkg/api/resource/scale_int_test.go | 85 ++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 pkg/api/resource/scale_int.go create mode 100644 pkg/api/resource/scale_int_test.go diff --git a/pkg/api/resource/quantity.go b/pkg/api/resource/quantity.go index 5b4a99dbc4b..6c2bcede319 100644 --- a/pkg/api/resource/quantity.go +++ b/pkg/api/resource/quantity.go @@ -377,8 +377,7 @@ func (q *Quantity) Value() int64 { if q.Amount == nil { return 0 } - tmp := &inf.Dec{} - return tmp.Round(q.Amount, 0, inf.RoundUp).UnscaledBig().Int64() + return scaledValue(q.Amount.UnscaledBig(), int(q.Amount.Scale()), 0) } // MilliValue returns the value of q * 1000; this could overflow an int64; @@ -387,8 +386,7 @@ func (q *Quantity) MilliValue() int64 { if q.Amount == nil { return 0 } - tmp := &inf.Dec{} - return tmp.Round(tmp.Mul(q.Amount, decThousand), 0, inf.RoundUp).UnscaledBig().Int64() + return scaledValue(q.Amount.UnscaledBig(), int(q.Amount.Scale()), 3) } // Set sets q's value to be value. diff --git a/pkg/api/resource/scale_int.go b/pkg/api/resource/scale_int.go new file mode 100644 index 00000000000..173de1a2171 --- /dev/null +++ b/pkg/api/resource/scale_int.go @@ -0,0 +1,95 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "math" + "math/big" + "sync" +) + +var ( + // A sync pool to reduce allocation. + intPool sync.Pool + maxInt64 = big.NewInt(math.MaxInt64) +) + +func init() { + intPool.New = func() interface{} { + return &big.Int{} + } +} + +// scaledValue scales given unscaled value from scale to new Scale and returns +// an int64. It ALWAYS rounds up the result when scale down. The final result might +// overflow. +// +// scale, newScale represents the scale of the unscaled decimal. +// The mathematical value of the decimal is unscaled * 10**(-scale). +func scaledValue(unscaled *big.Int, scale, newScale int) int64 { + dif := scale - newScale + if dif == 0 { + return unscaled.Int64() + } + + // Handle scale up + // This is an easy case, we do not need to care about rounding and overflow. + // If any intermediate operation causes overflow, the result will overflow. + if dif < 0 { + return unscaled.Int64() * int64(math.Pow10(-dif)) + } + + // Handle scale down + // We have to be careful about the intermediate operations. + + // fast path when unscaled < max.Int64 and exp(10,dif) < max.Int64 + const log10MaxInt64 = 19 + if unscaled.Cmp(maxInt64) < 0 && dif < log10MaxInt64 { + divide := int64(math.Pow10(dif)) + result := unscaled.Int64() / divide + mod := unscaled.Int64() % divide + if mod != 0 { + return result + 1 + } + return result + } + + // We should only convert back to int64 when getting the result. + divisor := intPool.Get().(*big.Int) + exp := intPool.Get().(*big.Int) + result := intPool.Get().(*big.Int) + defer func() { + intPool.Put(divisor) + intPool.Put(exp) + intPool.Put(result) + }() + + // divisor = 10^(dif) + // TODO: create loop up table if exp costs too much. + divisor.Exp(bigTen, exp.SetInt64(int64(dif)), nil) + // reuse exp + remainder := exp + + // result = unscaled / divisor + // remainder = unscaled % divisor + result.DivMod(unscaled, divisor, remainder) + if remainder.Sign() != 0 { + return result.Int64() + 1 + } + + return result.Int64() +} diff --git a/pkg/api/resource/scale_int_test.go b/pkg/api/resource/scale_int_test.go new file mode 100644 index 00000000000..558b196f674 --- /dev/null +++ b/pkg/api/resource/scale_int_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "math" + "math/big" + "testing" +) + +func TestScaledValue(t *testing.T) { + tests := []struct { + unscaled *big.Int + scale int + newScale int + + want int64 + }{ + // remain scale + {big.NewInt(1000), 0, 0, 1000}, + + // scale down + {big.NewInt(1000), 0, -3, 1}, + {big.NewInt(1000), 3, 0, 1}, + {big.NewInt(0), 3, 0, 0}, + + // always round up + {big.NewInt(999), 3, 0, 1}, + {big.NewInt(500), 3, 0, 1}, + {big.NewInt(499), 3, 0, 1}, + {big.NewInt(1), 3, 0, 1}, + // large scaled value does not lose precision + {big.NewInt(0).Sub(maxInt64, bigOne), 1, 0, (math.MaxInt64-1)/10 + 1}, + // large intermidiate result. + {big.NewInt(1).Exp(big.NewInt(10), big.NewInt(100), nil), 100, 0, 1}, + + // scale up + {big.NewInt(0), 0, 3, 0}, + {big.NewInt(1), 0, 3, 1000}, + {big.NewInt(1), -3, 0, 1000}, + {big.NewInt(1000), -3, 2, 100000000}, + {big.NewInt(0).Div(big.NewInt(math.MaxInt64), bigThousand), 0, 3, + (math.MaxInt64 / 1000) * 1000}, + } + + for i, tt := range tests { + old := (&big.Int{}).Set(tt.unscaled) + got := scaledValue(tt.unscaled, tt.scale, tt.newScale) + if got != tt.want { + t.Errorf("#%d: got = %v, want %v", i, got, tt.want) + } + if tt.unscaled.Cmp(old) != 0 { + t.Errorf("#%d: unscaled = %v, want %v", i, tt.unscaled, old) + } + } +} + +func BenchmarkScaledValueSmall(b *testing.B) { + s := big.NewInt(1000) + for i := 0; i < b.N; i++ { + scaledValue(s, 3, 0) + } +} + +func BenchmarkScaledValueLarge(b *testing.B) { + s := big.NewInt(math.MaxInt64) + s.Mul(s, big.NewInt(1000)) + for i := 0; i < b.N; i++ { + scaledValue(s, 10, 0) + } +}