1a715d7db5
The previous fix #21960 converted the float to an intermediate signed int, before attempting the uint conversion. Although this works, this doesn't guarantee that other architectures will work the same.
271 lines
8.2 KiB
Go
271 lines
8.2 KiB
Go
// Copyright 2020 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"math"
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/common/mclock"
|
|
)
|
|
|
|
// ExpiredValue is a scalar value that is continuously expired (decreased
|
|
// exponentially) based on the provided logarithmic expiration offset value.
|
|
//
|
|
// The formula for value calculation is: base*2^(exp-logOffset). In order to
|
|
// simplify the calculation of ExpiredValue, its value is expressed in the form
|
|
// of an exponent with a base of 2.
|
|
//
|
|
// Also here is a trick to reduce a lot of calculations. In theory, when a value X
|
|
// decays over time and then a new value Y is added, the final result should be
|
|
// X*2^(exp-logOffset)+Y. However it's very hard to represent in memory.
|
|
// So the trick is using the idea of inflation instead of exponential decay. At this
|
|
// moment the temporary value becomes: X*2^exp+Y*2^logOffset_1, apply the exponential
|
|
// decay when we actually want to calculate the value.
|
|
//
|
|
// e.g.
|
|
// t0: V = 100
|
|
// t1: add 30, inflationary value is: 100 + 30/0.3, 0.3 is the decay coefficient
|
|
// t2: get value, decay coefficient is 0.2 now, final result is: 200*0.2 = 40
|
|
type ExpiredValue struct {
|
|
Base, Exp uint64 // rlp encoding works by default
|
|
}
|
|
|
|
// ExpirationFactor is calculated from logOffset. 1 <= Factor < 2 and Factor*2^Exp
|
|
// describes the multiplier applicable for additions and the divider for readouts.
|
|
// If logOffset changes slowly then it saves some expensive operations to not calculate
|
|
// them for each addition and readout but cache this intermediate form for some time.
|
|
// It is also useful for structures where multiple values are expired with the same
|
|
// Expirer.
|
|
type ExpirationFactor struct {
|
|
Exp uint64
|
|
Factor float64
|
|
}
|
|
|
|
// ExpFactor calculates ExpirationFactor based on logOffset
|
|
func ExpFactor(logOffset Fixed64) ExpirationFactor {
|
|
return ExpirationFactor{Exp: logOffset.ToUint64(), Factor: logOffset.Fraction().Pow2()}
|
|
}
|
|
|
|
// Value calculates the expired value based on a floating point base and integer
|
|
// power-of-2 exponent. This function should be used by multi-value expired structures.
|
|
func (e ExpirationFactor) Value(base float64, exp uint64) float64 {
|
|
return base / e.Factor * math.Pow(2, float64(int64(exp-e.Exp)))
|
|
}
|
|
|
|
// value calculates the value at the given moment.
|
|
func (e ExpiredValue) Value(logOffset Fixed64) uint64 {
|
|
offset := Uint64ToFixed64(e.Exp) - logOffset
|
|
return uint64(float64(e.Base) * offset.Pow2())
|
|
}
|
|
|
|
// add adds a signed value at the given moment
|
|
func (e *ExpiredValue) Add(amount int64, logOffset Fixed64) int64 {
|
|
integer, frac := logOffset.ToUint64(), logOffset.Fraction()
|
|
factor := frac.Pow2()
|
|
base := factor * float64(amount)
|
|
if integer < e.Exp {
|
|
base /= math.Pow(2, float64(e.Exp-integer))
|
|
}
|
|
if integer > e.Exp {
|
|
e.Base >>= (integer - e.Exp)
|
|
e.Exp = integer
|
|
}
|
|
if base >= 0 || uint64(-base) <= e.Base {
|
|
// The conversion from negative float64 to
|
|
// uint64 is undefined in golang, and doesn't
|
|
// work with ARMv8. More details at:
|
|
// https://github.com/golang/go/issues/43047
|
|
if base >= 0 {
|
|
e.Base += uint64(base)
|
|
} else {
|
|
e.Base -= uint64(-base)
|
|
}
|
|
return amount
|
|
}
|
|
net := int64(-float64(e.Base) / factor)
|
|
e.Base = 0
|
|
return net
|
|
}
|
|
|
|
// addExp adds another ExpiredValue
|
|
func (e *ExpiredValue) AddExp(a ExpiredValue) {
|
|
if e.Exp > a.Exp {
|
|
a.Base >>= (e.Exp - a.Exp)
|
|
}
|
|
if e.Exp < a.Exp {
|
|
e.Base >>= (a.Exp - e.Exp)
|
|
e.Exp = a.Exp
|
|
}
|
|
e.Base += a.Base
|
|
}
|
|
|
|
// subExp subtracts another ExpiredValue
|
|
func (e *ExpiredValue) SubExp(a ExpiredValue) {
|
|
if e.Exp > a.Exp {
|
|
a.Base >>= (e.Exp - a.Exp)
|
|
}
|
|
if e.Exp < a.Exp {
|
|
e.Base >>= (a.Exp - e.Exp)
|
|
e.Exp = a.Exp
|
|
}
|
|
if e.Base > a.Base {
|
|
e.Base -= a.Base
|
|
} else {
|
|
e.Base = 0
|
|
}
|
|
}
|
|
|
|
// IsZero returns true if the value is zero
|
|
func (e *ExpiredValue) IsZero() bool {
|
|
return e.Base == 0
|
|
}
|
|
|
|
// LinearExpiredValue is very similar with the expiredValue which the value
|
|
// will continuously expired. But the different part is it's expired linearly.
|
|
type LinearExpiredValue struct {
|
|
Offset uint64 // The latest time offset
|
|
Val uint64 // The remaining value, can never be negative
|
|
Rate mclock.AbsTime `rlp:"-"` // Expiration rate(by nanosecond), will ignored by RLP
|
|
}
|
|
|
|
// value calculates the value at the given moment. This function always has the
|
|
// assumption that the given timestamp shouldn't less than the recorded one.
|
|
func (e LinearExpiredValue) Value(now mclock.AbsTime) uint64 {
|
|
offset := uint64(now / e.Rate)
|
|
if e.Offset < offset {
|
|
diff := offset - e.Offset
|
|
if e.Val >= diff {
|
|
e.Val -= diff
|
|
} else {
|
|
e.Val = 0
|
|
}
|
|
}
|
|
return e.Val
|
|
}
|
|
|
|
// add adds a signed value at the given moment. This function always has the
|
|
// assumption that the given timestamp shouldn't less than the recorded one.
|
|
func (e *LinearExpiredValue) Add(amount int64, now mclock.AbsTime) uint64 {
|
|
offset := uint64(now / e.Rate)
|
|
if e.Offset < offset {
|
|
diff := offset - e.Offset
|
|
if e.Val >= diff {
|
|
e.Val -= diff
|
|
} else {
|
|
e.Val = 0
|
|
}
|
|
e.Offset = offset
|
|
}
|
|
if amount < 0 && uint64(-amount) > e.Val {
|
|
e.Val = 0
|
|
} else {
|
|
e.Val = uint64(int64(e.Val) + amount)
|
|
}
|
|
return e.Val
|
|
}
|
|
|
|
// ValueExpirer controls value expiration rate
|
|
type ValueExpirer interface {
|
|
SetRate(now mclock.AbsTime, rate float64)
|
|
SetLogOffset(now mclock.AbsTime, logOffset Fixed64)
|
|
LogOffset(now mclock.AbsTime) Fixed64
|
|
}
|
|
|
|
// Expirer changes logOffset with a linear rate which can be changed during operation.
|
|
// It is not thread safe, if access by multiple goroutines is needed then it should be
|
|
// encapsulated into a locked structure.
|
|
// Note that if neither SetRate nor SetLogOffset are used during operation then LogOffset
|
|
// is thread safe.
|
|
type Expirer struct {
|
|
lock sync.RWMutex
|
|
logOffset Fixed64
|
|
rate float64
|
|
lastUpdate mclock.AbsTime
|
|
}
|
|
|
|
// SetRate changes the expiration rate which is the inverse of the time constant in
|
|
// nanoseconds.
|
|
func (e *Expirer) SetRate(now mclock.AbsTime, rate float64) {
|
|
e.lock.Lock()
|
|
defer e.lock.Unlock()
|
|
|
|
dt := now - e.lastUpdate
|
|
if dt > 0 {
|
|
e.logOffset += Fixed64(logToFixedFactor * float64(dt) * e.rate)
|
|
}
|
|
e.lastUpdate = now
|
|
e.rate = rate
|
|
}
|
|
|
|
// SetLogOffset sets logOffset instantly.
|
|
func (e *Expirer) SetLogOffset(now mclock.AbsTime, logOffset Fixed64) {
|
|
e.lock.Lock()
|
|
defer e.lock.Unlock()
|
|
|
|
e.lastUpdate = now
|
|
e.logOffset = logOffset
|
|
}
|
|
|
|
// LogOffset returns the current logarithmic offset.
|
|
func (e *Expirer) LogOffset(now mclock.AbsTime) Fixed64 {
|
|
e.lock.RLock()
|
|
defer e.lock.RUnlock()
|
|
|
|
dt := now - e.lastUpdate
|
|
if dt <= 0 {
|
|
return e.logOffset
|
|
}
|
|
return e.logOffset + Fixed64(logToFixedFactor*float64(dt)*e.rate)
|
|
}
|
|
|
|
// fixedFactor is the fixed point multiplier factor used by Fixed64.
|
|
const fixedFactor = 0x1000000
|
|
|
|
// Fixed64 implements 64-bit fixed point arithmetic functions.
|
|
type Fixed64 int64
|
|
|
|
// Uint64ToFixed64 converts uint64 integer to Fixed64 format.
|
|
func Uint64ToFixed64(f uint64) Fixed64 {
|
|
return Fixed64(f * fixedFactor)
|
|
}
|
|
|
|
// float64ToFixed64 converts float64 to Fixed64 format.
|
|
func Float64ToFixed64(f float64) Fixed64 {
|
|
return Fixed64(f * fixedFactor)
|
|
}
|
|
|
|
// toUint64 converts Fixed64 format to uint64.
|
|
func (f64 Fixed64) ToUint64() uint64 {
|
|
return uint64(f64) / fixedFactor
|
|
}
|
|
|
|
// fraction returns the fractional part of a Fixed64 value.
|
|
func (f64 Fixed64) Fraction() Fixed64 {
|
|
return f64 % fixedFactor
|
|
}
|
|
|
|
var (
|
|
logToFixedFactor = float64(fixedFactor) / math.Log(2)
|
|
fixedToLogFactor = math.Log(2) / float64(fixedFactor)
|
|
)
|
|
|
|
// pow2Fixed returns the base 2 power of the fixed point value.
|
|
func (f64 Fixed64) Pow2() float64 {
|
|
return math.Exp(float64(f64) * fixedToLogFactor)
|
|
}
|