github.com/jgbaldwinbrown/perf@v0.1.1/benchunit/scale.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package benchunit 6 7 import ( 8 "fmt" 9 "math" 10 "strconv" 11 ) 12 13 // A Scaler represents a scaling factor for a number and 14 // its scientific representation. 15 type Scaler struct { 16 Prec int // Digits after the decimal point 17 Factor float64 // Unscaled value of 1 Prefix (e.g., 1 k => 1000) 18 Prefix string // Unit prefix ("k", "M", "Ki", etc) 19 } 20 21 // Format formats val and appends the unit prefix according to the given scale. 22 // For example, if the Scaler has class Decimal, Format(123456789) 23 // returns "123.4M". 24 // 25 // If the value has units, be sure to tidy it first (see Tidy). 26 // Otherwise, this can result in nonsense units when the result is 27 // displayed with its units; for example, representing 123456789 ns as 28 // 123.4M ns ("megananoseconds", aka milliseconds). 29 func (s Scaler) Format(val float64) string { 30 buf := make([]byte, 0, 20) 31 buf = strconv.AppendFloat(buf, val/s.Factor, 'f', s.Prec, 64) 32 buf = append(buf, s.Prefix...) 33 return string(buf) 34 } 35 36 // NoOpScaler is a Scaler that formats numbers with the smallest 37 // number of digits necessary to capture the exact value, and no 38 // prefix. This is intended for when the output will be consumed by 39 // another program, such as when producing CSV format. 40 var NoOpScaler = Scaler{-1, 1, ""} 41 42 type factor struct { 43 factor float64 44 prefix string 45 // Thresholds for 100.0, 10.00, 1.000. 46 t100, t10, t1 float64 47 } 48 49 var siFactors = mkSIFactors() 50 var iecFactors = mkIECFactors() 51 var sigfigs, sigfigsBase = mkSigfigs() 52 53 func mkSIFactors() []factor { 54 // To ensure that the thresholds for printing values with 55 // various factors exactly match how printing itself will 56 // round, we construct the thresholds by parsing the printed 57 // representation. 58 var factors []factor 59 exp := 12 60 for _, p := range []string{"T", "G", "M", "k", "", "m", "ยต", "n"} { 61 t100, _ := strconv.ParseFloat(fmt.Sprintf("99.995e%d", exp), 64) 62 t10, _ := strconv.ParseFloat(fmt.Sprintf("9.9995e%d", exp), 64) 63 t1, _ := strconv.ParseFloat(fmt.Sprintf(".99995e%d", exp), 64) 64 factors = append(factors, factor{math.Pow(10, float64(exp)), p, t100, t10, t1}) 65 exp -= 3 66 } 67 return factors 68 } 69 70 func mkIECFactors() []factor { 71 var factors []factor 72 exp := 40 73 // ISO/IEC 80000 doesn't specify fractional prefixes. They're 74 // meaningful for things like "B/sec", but "1 /KiB/s" looks a 75 // lot more confusing than bottoming out at "B" and printing 76 // with more precision, such as "0.001 B/s". 77 // 78 // Because t1 of one factor represents 1024 of the next 79 // smaller factor, we'll render values in the range [1000, 80 // 1024) using the smaller factor. E.g., 1020 * 1024 will be 81 // rendered as 1020 KiB, not 0.996 MiB. This is intentional as 82 // it achieves higher precision, avoids rendering scaled 83 // values below 1, and seems generally less confusing for 84 // binary factors. 85 for _, p := range []string{"Ti", "Gi", "Mi", "Ki", ""} { 86 t100, _ := strconv.ParseFloat(fmt.Sprintf("0x1.8ffae147ae148p%d", 6+exp), 64) // 99.995 87 t10, _ := strconv.ParseFloat(fmt.Sprintf("0x1.3ffbe76c8b439p%d", 3+exp), 64) // 9.9995 88 t1, _ := strconv.ParseFloat(fmt.Sprintf("0x1.fff972474538fp%d", -1+exp), 64) // .99995 89 factors = append(factors, factor{math.Pow(2, float64(exp)), p, t100, t10, t1}) 90 exp -= 10 91 } 92 return factors 93 } 94 95 func mkSigfigs() ([]float64, int) { 96 var sigfigs []float64 97 // Print up to 10 digits after the decimal place. 98 for exp := -1; exp > -9; exp-- { 99 thresh, _ := strconv.ParseFloat(fmt.Sprintf("9.9995e%d", exp), 64) 100 sigfigs = append(sigfigs, thresh) 101 } 102 // sigfigs[0] is the threshold for 3 digits after the decimal. 103 return sigfigs, 3 104 } 105 106 // Scale formats val using at least three significant digits, 107 // appending an SI or binary prefix. See Scaler.Format for details. 108 func Scale(val float64, cls Class) string { 109 return CommonScale([]float64{val}, cls).Format(val) 110 } 111 112 // CommonScale returns a common Scaler to apply to all values in vals. 113 // This scale will show at least three significant digits for every 114 // value. 115 func CommonScale(vals []float64, cls Class) Scaler { 116 // The common scale is determined by the non-zero value 117 // closest to zero. 118 var min float64 119 for _, v := range vals { 120 v = math.Abs(v) 121 if v != 0 && (min == 0 || v < min) { 122 min = v 123 } 124 } 125 if min == 0 { 126 return Scaler{3, 1, ""} 127 } 128 129 var factors []factor 130 switch cls { 131 default: 132 panic(fmt.Sprintf("bad Class %v", cls)) 133 case Decimal: 134 factors = siFactors 135 case Binary: 136 factors = iecFactors 137 } 138 139 for _, factor := range factors { 140 switch { 141 case min >= factor.t100: 142 return Scaler{1, factor.factor, factor.prefix} 143 case min >= factor.t10: 144 return Scaler{2, factor.factor, factor.prefix} 145 case min >= factor.t1: 146 return Scaler{3, factor.factor, factor.prefix} 147 } 148 } 149 150 // The value is less than the smallest factor. Print it using 151 // the smallest factor and more precision to achieve the 152 // desired sigfigs. 153 factor := factors[len(factors)-1] 154 val := min / factor.factor 155 for i, thresh := range sigfigs { 156 if val >= thresh || i == len(sigfigs)-1 { 157 return Scaler{i + sigfigsBase, factor.factor, factor.prefix} 158 } 159 } 160 161 panic("not reachable") 162 }