| // Copyright 2022 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package benchunit | |
| import ( | |
| "fmt" | |
| "math" | |
| "strconv" | |
| ) | |
| // A Scaler represents a scaling factor for a number and | |
| // its scientific representation. | |
| type Scaler struct { | |
| Prec int // Digits after the decimal point | |
| Factor float64 // Unscaled value of 1 Prefix (e.g., 1 k => 1000) | |
| Prefix string // Unit prefix ("k", "M", "Ki", etc) | |
| } | |
| // Format formats val and appends the unit prefix according to the given scale. | |
| // For example, if the Scaler has class Decimal, Format(123456789) | |
| // returns "123.4M". | |
| // | |
| // If the value has units, be sure to tidy it first (see Tidy). | |
| // Otherwise, this can result in nonsense units when the result is | |
| // displayed with its units; for example, representing 123456789 ns as | |
| // 123.4M ns ("megananoseconds", aka milliseconds). | |
| func (s Scaler) Format(val float64) string { | |
| buf := make([]byte, 0, 20) | |
| buf = strconv.AppendFloat(buf, val/s.Factor, 'f', s.Prec, 64) | |
| buf = append(buf, s.Prefix...) | |
| return string(buf) | |
| } | |
| // NoOpScaler is a Scaler that formats numbers with the smallest | |
| // number of digits necessary to capture the exact value, and no | |
| // prefix. This is intended for when the output will be consumed by | |
| // another program, such as when producing CSV format. | |
| var NoOpScaler = Scaler{-1, 1, ""} | |
| type factor struct { | |
| factor float64 | |
| prefix string | |
| // Thresholds for 100.0, 10.00, 1.000. | |
| t100, t10, t1 float64 | |
| } | |
| var siFactors = mkSIFactors() | |
| var iecFactors = mkIECFactors() | |
| var sigfigs, sigfigsBase = mkSigfigs() | |
| func mkSIFactors() []factor { | |
| // To ensure that the thresholds for printing values with | |
| // various factors exactly match how printing itself will | |
| // round, we construct the thresholds by parsing the printed | |
| // representation. | |
| var factors []factor | |
| exp := 12 | |
| for _, p := range []string{"T", "G", "M", "k", "", "m", "ยต", "n"} { | |
| t100, _ := strconv.ParseFloat(fmt.Sprintf("99.995e%d", exp), 64) | |
| t10, _ := strconv.ParseFloat(fmt.Sprintf("9.9995e%d", exp), 64) | |
| t1, _ := strconv.ParseFloat(fmt.Sprintf(".99995e%d", exp), 64) | |
| factors = append(factors, factor{math.Pow(10, float64(exp)), p, t100, t10, t1}) | |
| exp -= 3 | |
| } | |
| return factors | |
| } | |
| func mkIECFactors() []factor { | |
| var factors []factor | |
| exp := 40 | |
| // ISO/IEC 80000 doesn't specify fractional prefixes. They're | |
| // meaningful for things like "B/sec", but "1 /KiB/s" looks a | |
| // lot more confusing than bottoming out at "B" and printing | |
| // with more precision, such as "0.001 B/s". | |
| // | |
| // Because t1 of one factor represents 1024 of the next | |
| // smaller factor, we'll render values in the range [1000, | |
| // 1024) using the smaller factor. E.g., 1020 * 1024 will be | |
| // rendered as 1020 KiB, not 0.996 MiB. This is intentional as | |
| // it achieves higher precision, avoids rendering scaled | |
| // values below 1, and seems generally less confusing for | |
| // binary factors. | |
| for _, p := range []string{"Ti", "Gi", "Mi", "Ki", ""} { | |
| t100, _ := strconv.ParseFloat(fmt.Sprintf("0x1.8ffae147ae148p%d", 6+exp), 64) // 99.995 | |
| t10, _ := strconv.ParseFloat(fmt.Sprintf("0x1.3ffbe76c8b439p%d", 3+exp), 64) // 9.9995 | |
| t1, _ := strconv.ParseFloat(fmt.Sprintf("0x1.fff972474538fp%d", -1+exp), 64) // .99995 | |
| factors = append(factors, factor{math.Pow(2, float64(exp)), p, t100, t10, t1}) | |
| exp -= 10 | |
| } | |
| return factors | |
| } | |
| func mkSigfigs() ([]float64, int) { | |
| var sigfigs []float64 | |
| // Print up to 10 digits after the decimal place. | |
| for exp := -1; exp > -9; exp-- { | |
| thresh, _ := strconv.ParseFloat(fmt.Sprintf("9.9995e%d", exp), 64) | |
| sigfigs = append(sigfigs, thresh) | |
| } | |
| // sigfigs[0] is the threshold for 3 digits after the decimal. | |
| return sigfigs, 3 | |
| } | |
| // Scale formats val using at least three significant digits, | |
| // appending an SI or binary prefix. See Scaler.Format for details. | |
| func Scale(val float64, cls Class) string { | |
| return CommonScale([]float64{val}, cls).Format(val) | |
| } | |
| // CommonScale returns a common Scaler to apply to all values in vals. | |
| // This scale will show at least three significant digits for every | |
| // value. | |
| func CommonScale(vals []float64, cls Class) Scaler { | |
| // The common scale is determined by the non-zero value | |
| // closest to zero. | |
| var min float64 | |
| for _, v := range vals { | |
| v = math.Abs(v) | |
| if v != 0 && (min == 0 || v < min) { | |
| min = v | |
| } | |
| } | |
| if min == 0 { | |
| return Scaler{3, 1, ""} | |
| } | |
| var factors []factor | |
| switch cls { | |
| default: | |
| panic(fmt.Sprintf("bad Class %v", cls)) | |
| case Decimal: | |
| factors = siFactors | |
| case Binary: | |
| factors = iecFactors | |
| } | |
| for _, factor := range factors { | |
| switch { | |
| case min >= factor.t100: | |
| return Scaler{1, factor.factor, factor.prefix} | |
| case min >= factor.t10: | |
| return Scaler{2, factor.factor, factor.prefix} | |
| case min >= factor.t1: | |
| return Scaler{3, factor.factor, factor.prefix} | |
| } | |
| } | |
| // The value is less than the smallest factor. Print it using | |
| // the smallest factor and more precision to achieve the | |
| // desired sigfigs. | |
| factor := factors[len(factors)-1] | |
| val := min / factor.factor | |
| for i, thresh := range sigfigs { | |
| if val >= thresh || i == len(sigfigs)-1 { | |
| return Scaler{i + sigfigsBase, factor.factor, factor.prefix} | |
| } | |
| } | |
| panic("not reachable") | |
| } |