From a90cbd7a11c11c80fd66cfe70f87c9e97e646f06 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 27 Jun 2022 17:06:56 -0700 Subject: [PATCH 01/13] Change the exponential histogram boundary condition --- specification/metrics/data-model.md | 103 +++++++++++++++++++++------- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 7376b022c35..8f7fc6cb76a 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -599,14 +599,14 @@ downscale) without introducing error. #### Exponential Buckets The ExponentialHistogram bucket identified by `index`, a signed -integer, represents values in the population that are greater than or -equal to `base**index` and less than `base**(index+1)`. Note that the -ExponentialHistogram specifies a lower-inclusive bound while the -explicit-boundary Histogram specifies an upper-inclusive bound. +integer, represents values in the population that are greater than +`base**index` and less than or equal to `base**(index+1)`. The positive and negative ranges of the histogram are expressed -separately. Negative values are mapped by their absolute value -into the negative range using the same scale as the positive range. +separately. Negative values are mapped by their absolute value into +the negative range using the same scale as the positive range. Note +that in the negative range, therefore, histogram buckets use +lower-inclusive boundaries. Each range of the ExponentialHistogram data point uses a dense representation of the buckets, where a range of buckets is expressed @@ -616,9 +616,9 @@ index `offset+i`. For a given range, positive or negative: -- Bucket index `0` counts measurements in the range `[1, base)` -- Positive indexes correspond with absolute values greater or equal to `base` -- Negative indexes correspond with absolute values less than 1 +- Bucket index `0` counts measurements in the range `(1, base]` +- Positive indexes correspond with absolute values greater than `base` +- Negative indexes correspond with absolute values less than or equal to 1 - There are `2**scale` buckets between successive powers of 2. For example, with `scale=3` there are `2**3` buckets between 1 and 2. @@ -650,6 +650,18 @@ that have been rounded to zero. #### Producer Expectations +Producers MAY use an inexect mapping function because in the general +case: + +1. Exact mapping functions are substantially more complex to implement. +2. Boundaries cannot be exactly represented as floating point numbers for all scales. + +The statement that a histogram bucket includes values exactly equal to +its upper boundary is for the benefit of table-lookup based +implementations, where exactness is a desired outcome. Generally, +producers SHOULD use a mapping function with a difference of at most 1 +from the correct result for all inputs. + The ExponentialHistogram design makes it possible to express values that are too large or small to be represented in the 64 bit "double" floating point format. Certain values for `scale`, while meaningful, @@ -704,10 +716,8 @@ double-width floating point values have indices in the range `[-1074, -1023]`. This may be written as: ```golang -// GetExponent extracts the normalized base-2 fractional exponent. -// Let the value be represented as `1.significand x 2**exponent`, -// this returns `exponent`. Not defined for 0, Inf, or NaN values. -func GetExponent(value float64) int32 { +// MapToIndexScale0 computes a bucket index at scale 0. +func MapToIndexScale0(value float64) int32 { rawBits := math.Float64bits(value) rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth rawSignificand := rawBits & SignificandMask @@ -716,7 +726,12 @@ func GetExponent(value float64) int32 { // unless value is zero. rawExponent -= int64(bits.LeadingZeros64(rawSignificand) - 12) } - return int32(rawExponent - ExponentBias) + ieeeExponent := int32(rawExponent - ExponentBias) + if rawSignificand == 0 { + // Special case for power-of-two boundary: subtract one. + return ieeeExponent - 1 + } + return ieeeExponent } ``` @@ -724,13 +739,17 @@ Implementations are permitted to round subnormal values up to the smallest normal value, which may permit the use of a built-in function: ```golang - -func GetExponent(value float64) int { +// MapToIndexScale0 computes a bucket index at scale 0. +func MapToIndexScale0(value float64) int { // Note: Frexp() rounds submnormal values to the smallest normal // value and returns an exponent corresponding to fractions in the - // range [0.5, 1), whereas we want [1, 2), so subtract 1 from the + // range [0.5, 1), whereas we want (1, 2], so subtract 1 from the // exponent. - _, exp := math.Frexp(value) + frac, exp := math.Frexp(value) + if frac == 0.5 { + // Special case for power-of-two boundary: subtract one. + exp-- + } return exp - 1 } ``` @@ -743,13 +762,20 @@ by `-scale`. Note that because of sign extension, this shift performs correct rounding for the negative indices. This may be written as: ```golang - return GetExponent(value) >> -scale +// MapToIndexNegativeScale computes a bucket index for scale 0. +func MapToIndexNegativeScale(value float64) int { + return MapToIndexScale0(value) >> -scale +} ``` The reverse mapping function is: ```golang +// LowerBoundaryNegativeScale computes the lower boundary for index +// with scale < 0. +func LowerBoundaryNegativeScale(index int) { return math.Ldexp(1, index << -scale) +} ``` Note that the reverse mapping function is expected to produce @@ -767,18 +793,42 @@ compute the bucket index. A multiplicative factor equal to `2**scale example: ```golang +// MapToIndex for any scale. +func MapToIndex(value float64) int { scaleFactor := math.Ldexp(math.Log2E, scale) - return math.Floor(math.Log(value) * scaleFactor) + return math.Ceil(math.Log(value) * scaleFactor) - 1 +} ``` -Note that in the example Golang code above, the built-in `math.Log2E` -is defined as the inverse of the natural logarithm of 2, i.e., `1 / ln(2)`. +The expression `math.Ceil(expr) - 1` rounds the calculated index up +and subtracts 1 to ensure the correct boundary inclusivity. Note thatl +in the example Golang code above, the built-in `math.Log2E` is defined +as the inverse of the natural logarithm of 2, i.e., `1 / ln(2)`. -The reverse mapping function is: +The use of `math.Ceil(expr) - 1` is not guaranteed to be exact, even +for power-of-two inputs. Since it is relatively simple to check for +exact powers of two, implementations SHOULD consider such a special +case: + +``` +// MapToIndex for any scale, exact for powers of two. +func MapToIndex(value float64) int { + if getSignificand(value) == 0 { + return (MapToIndexScale0(value) << scale) - 1 + } + scaleFactor := math.Ldexp(math.Log2E, scale) + return math.Ceil(math.Log(value) * scaleFactor) - 1 +} +``` + +The reverse mapping function for positive scales is: ```golang +// LowerBoundary computes the bucket boundary for positive scales. +func LowerBoundary(index int) float64 { inverseFactor := math.Ldexp(math.Ln2, -scale) return math.Exp(index * inverseFactor), nil +} ``` Implementations are expected to verify that their mapping function and @@ -791,12 +841,19 @@ reference implementation, for example, the above formula computes to subtract `1< Date: Thu, 7 Jul 2022 14:22:09 -0700 Subject: [PATCH 02/13] remove lookup table; untabify; derive the scaling factor for positive scales; clarify why two different expositions; typos --- specification/metrics/data-model.md | 93 ++++++++++++++++++----------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 654de4999d5..3081b34ddd1 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -732,8 +732,8 @@ func MapToIndexScale0(value float64) int32 { } ieeeExponent := int32(rawExponent - ExponentBias) if rawSignificand == 0 { - // Special case for power-of-two boundary: subtract one. - return ieeeExponent - 1 + // Special case for power-of-two boundary: subtract one. + return ieeeExponent - 1 } return ieeeExponent } @@ -747,28 +747,31 @@ smallest normal value, which may permit the use of a built-in function: func MapToIndexScale0(value float64) int { // Note: Frexp() rounds submnormal values to the smallest normal // value and returns an exponent corresponding to fractions in the - // range [0.5, 1), whereas we want (1, 2], so subtract 1 from the - // exponent. + // range [0.5, 1), whereas an exponent for the range [1, 2), so + // subtract 1 from the exponent immediately. frac, exp := math.Frexp(value) - if frac == 0.5 { - // Special case for power-of-two boundary: subtract one. - exp-- - } - return exp - 1 + exp-- + + if frac == 0.5 { + // Special case for powers of two: they fall into the bucket + // numbered one less. + exp-- + } + return exp } ``` ##### Negative Scale: Extract and Shift the Exponent For negative scales, the index of a value equals the normalized -base-2 exponent (as by `GetExponent()` above) shifted to the right +base-2 exponent (as by `MapToIndexScale0()` above) shifted to the right by `-scale`. Note that because of sign extension, this shift performs correct rounding for the negative indices. This may be written as: ```golang -// MapToIndexNegativeScale computes a bucket index for scale 0. +// MapToIndexNegativeScale computes a bucket index for scales <= 0. func MapToIndexNegativeScale(value float64) int { - return MapToIndexScale0(value) >> -scale + return MapToIndexScale0(value) >> -scale } ``` @@ -776,7 +779,7 @@ The reverse mapping function is: ```golang // LowerBoundaryNegativeScale computes the lower boundary for index -// with scale < 0. +// with scales <= 0. func LowerBoundaryNegativeScale(index int) { return math.Ldexp(1, index << -scale) } @@ -791,10 +794,31 @@ boundary `0x1p-1024`. ##### All Scales: Use the Logarithm Function -For any scale, the built-in natural logarithm function can be used to -compute the bucket index. A multiplicative factor equal to `2**scale -/ ln(2)` proves useful (where `ln()` is the natural logarithm), for -example: +The mapping and reverse-mapping functions for scale zero and negative +scales above are recommended because they are exact. At these scales, +`math.Log()` could be inaccurate and more expensive than directly +calculating the bucket index. The methods in this section MAY be used +at all scales, although they are definitely useful for positive +scales. + +The built-in natural logarithm function can be used to compute the +bucket index by applying a scaling factor, derived as follows. + +1. The exponential base is defined as `base = 2**(2**(-scale))` +2. We want `index` where `base**index < value <= base**(index+1)`. +3. Apply the logarithm corresponding with base, i.e., + `logbase(base**index) < logbase(value) <= logbase(base**(index+1))` +4. Rewrite using `logX(X**Y) == Y`: +5. Thus, `index < logbase(value) <= index+1` +6. Using the `Ceiling()` function to simplify the equation: `Ceiling(logbase(value)) == index+1` +7. Subtract one from each side: `index == Ceiling(logbase(value)) - 1` +8. Rewrite using `logX(Y) == logN(Y) / logN(X)` to use the natural logarithm +9. Thus, `index == Ceiling(log(value)/log(base)) - 1` +12. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). + +The scaling factor equals `2**scale / log(2)` can be written as +`math.Ldexp(math.Log2E, scale)` since the constant `math.Log2E` is +defined as `1/log(2)`. Putting this together: ```golang // MapToIndex for any scale. @@ -804,24 +828,27 @@ func MapToIndex(value float64) int { } ``` -The expression `math.Ceil(expr) - 1` rounds the calculated index up -and subtracts 1 to ensure the correct boundary inclusivity. Note thatl -in the example Golang code above, the built-in `math.Log2E` is defined -as the inverse of the natural logarithm of 2, i.e., `1 / ln(2)`. +The use of `math.Log()` to calculate the bucket index is not +guaranteed to be exactly correct near powers of two. Values near a +boundary could be mapped into the incorrect bucket due to inaccuracy. +Defining an exact mapping function is out of scope for this document. -The use of `math.Ceil(expr) - 1` is not guaranteed to be exact, even -for power-of-two inputs. Since it is relatively simple to check for -exact powers of two, implementations SHOULD consider such a special -case: +However, when inputs are an exact power of two, it is possible to +calculate the exactly corect bucket index. Since it is relatively +simple to check for exact powers of two, implementations SHOULD +apply such a special case: ``` // MapToIndex for any scale, exact for powers of two. func MapToIndex(value float64) int { - if getSignificand(value) == 0 { - return (MapToIndexScale0(value) << scale) - 1 - } + // Special case for power-of-two values. + if frac, exp := math.Frexp(value); frac == 0.5 { + return ((exp-1) << scale) - 1 + } scaleFactor := math.Ldexp(math.Log2E, scale) - return math.Ceil(math.Log(value) * scaleFactor) - 1 + // Note: math.Floor(value) equals math.Ceil(value)-1 when value + // is not a power of two, which is checked above. + return math.Floor(math.Log(value) * scaleFactor) } ``` @@ -845,7 +872,7 @@ reference implementation, for example, the above formula computes to subtract `1< Date: Thu, 7 Jul 2022 14:46:58 -0700 Subject: [PATCH 03/13] typo --- specification/metrics/data-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 3081b34ddd1..31e6f74f4f6 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -654,7 +654,7 @@ that have been rounded to zero. #### Producer Expectations -Producers MAY use an inexect mapping function because in the general +Producers MAY use an inexact mapping function because in the general case: 1. Exact mapping functions are substantially more complex to implement. From 76125e7ccfc0e77b3b0f54075beccd0491596a11 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 7 Jul 2022 14:50:57 -0700 Subject: [PATCH 04/13] remove mention of table lookup --- specification/metrics/data-model.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 31e6f74f4f6..0df1ef26ef0 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -660,11 +660,8 @@ case: 1. Exact mapping functions are substantially more complex to implement. 2. Boundaries cannot be exactly represented as floating point numbers for all scales. -The statement that a histogram bucket includes values exactly equal to -its upper boundary is for the benefit of table-lookup based -implementations, where exactness is a desired outcome. Generally, -producers SHOULD use a mapping function with a difference of at most 1 -from the correct result for all inputs. +Generally, producers SHOULD use a mapping function with an expected +difference of at most 1 from the correct result for all inputs. The ExponentialHistogram design makes it possible to express values that are too large or small to be represented in the 64 bit "double" From 53d26d61fda41260e124858d2d406da5bab1f852 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 7 Jul 2022 14:53:54 -0700 Subject: [PATCH 05/13] format subscripts --- specification/metrics/data-model.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 0df1ef26ef0..482c0c1d671 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -804,14 +804,14 @@ bucket index by applying a scaling factor, derived as follows. 1. The exponential base is defined as `base = 2**(2**(-scale))` 2. We want `index` where `base**index < value <= base**(index+1)`. 3. Apply the logarithm corresponding with base, i.e., - `logbase(base**index) < logbase(value) <= logbase(base**(index+1))` -4. Rewrite using `logX(X**Y) == Y`: -5. Thus, `index < logbase(value) <= index+1` -6. Using the `Ceiling()` function to simplify the equation: `Ceiling(logbase(value)) == index+1` -7. Subtract one from each side: `index == Ceiling(logbase(value)) - 1` -8. Rewrite using `logX(Y) == logN(Y) / logN(X)` to use the natural logarithm + `logbase(base**index) < logbase(value) <= logbase(base**(index+1))` +4. Rewrite using `logX(X**Y) == Y`: +5. Thus, `index < logbase(value) <= index+1` +6. Using the `Ceiling()` function to simplify the equation: `Ceiling(logbase(value)) == index+1` +7. Subtract one from each side: `index == Ceiling(logbase(value)) - 1` +8. Rewrite using `logX(Y) == logN(Y) / logN(X)` to use the natural logarithm 9. Thus, `index == Ceiling(log(value)/log(base)) - 1` -12. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). +10. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). The scaling factor equals `2**scale / log(2)` can be written as `math.Ldexp(math.Log2E, scale)` since the constant `math.Log2E` is From d65a07681b2652805c639ea0351c6376a69b8fa3 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 7 Jul 2022 14:58:46 -0700 Subject: [PATCH 06/13] deformat --- specification/metrics/data-model.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 482c0c1d671..dabeb30af44 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -801,17 +801,17 @@ scales. The built-in natural logarithm function can be used to compute the bucket index by applying a scaling factor, derived as follows. -1. The exponential base is defined as `base = 2**(2**(-scale))` +1. The exponential base is defined as `base == 2**(2**(-scale))` 2. We want `index` where `base**index < value <= base**(index+1)`. 3. Apply the logarithm corresponding with base, i.e., - `logbase(base**index) < logbase(value) <= logbase(base**(index+1))` -4. Rewrite using `logX(X**Y) == Y`: -5. Thus, `index < logbase(value) <= index+1` -6. Using the `Ceiling()` function to simplify the equation: `Ceiling(logbase(value)) == index+1` -7. Subtract one from each side: `index == Ceiling(logbase(value)) - 1` -8. Rewrite using `logX(Y) == logN(Y) / logN(X)` to use the natural logarithm + `log_base(base**index) < log_base(value) <= log_base(base**(index+1))` +4. Rewrite using `log_X(X**Y) == Y`: +5. Thus, `index < log_base(value) <= index+1` +6. Using the `Ceiling()` function to simplify the equation: `Ceiling(log_base(value)) == index+1` +7. Subtract one from each side: `index == Ceiling(log_base(value)) - 1` +8. Rewrite using `log_X(Y) == log_N(Y) / log_N(X)` to allow use of the natural logarithm 9. Thus, `index == Ceiling(log(value)/log(base)) - 1` -10. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). +12. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). The scaling factor equals `2**scale / log(2)` can be written as `math.Ldexp(math.Log2E, scale)` since the constant `math.Log2E` is From 969d0b650e635220d5729b89124a63c752685449 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 7 Jul 2022 15:00:34 -0700 Subject: [PATCH 07/13] moreformat --- specification/metrics/data-model.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index dabeb30af44..68ef6cdc3e7 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -799,12 +799,12 @@ at all scales, although they are definitely useful for positive scales. The built-in natural logarithm function can be used to compute the -bucket index by applying a scaling factor, derived as follows. +bucket index by applying a scaling factor, derived as follows. 1. The exponential base is defined as `base == 2**(2**(-scale))` 2. We want `index` where `base**index < value <= base**(index+1)`. -3. Apply the logarithm corresponding with base, i.e., - `log_base(base**index) < log_base(value) <= log_base(base**(index+1))` +3. Apply the base-`base` logarithm, i.e., + `log_base(base**index) < log_base(value) <= log_base(base**(index+1))` (where `log_X(Y)` indicates the base-`X` logarithm of `Y`) 4. Rewrite using `log_X(X**Y) == Y`: 5. Thus, `index < log_base(value) <= index+1` 6. Using the `Ceiling()` function to simplify the equation: `Ceiling(log_base(value)) == index+1` From abd536243c7c9f36b6dd6192005d1bf1b50682eb Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 7 Jul 2022 15:03:18 -0700 Subject: [PATCH 08/13] typo --- specification/metrics/data-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 68ef6cdc3e7..82b05a72e8b 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -862,7 +862,7 @@ func LowerBoundary(index int) float64 { Implementations are expected to verify that their mapping function and inverse mapping function are correct near the lowest and highest IEEE floating point values. A mathematically correct formula may produce -wrong result, because of accumulated floating point calculation error +the wrong result, because of accumulated floating point calculation error or underflow/overflow of intermediate results. In the Golang reference implementation, for example, the above formula computes `+Inf` for the maximum-index bucket. In this case, it is appropriate From 8a3719648e7888566b21714512bab37d1501bdfe Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 18 Jul 2022 11:13:35 -0700 Subject: [PATCH 09/13] Update specification/metrics/data-model.md Co-authored-by: David Ashpole --- specification/metrics/data-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 82b05a72e8b..74ae12c3f63 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -835,7 +835,7 @@ calculate the exactly corect bucket index. Since it is relatively simple to check for exact powers of two, implementations SHOULD apply such a special case: -``` +```golang // MapToIndex for any scale, exact for powers of two. func MapToIndex(value float64) int { // Special case for power-of-two values. From 8215aaf4bb423defcf3c1661b474d554f2c236e3 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 27 Jul 2022 11:53:53 -0700 Subject: [PATCH 10/13] reiley's fixes --- specification/metrics/data-model.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 4b2c7e612c7..ab35f804b2f 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -720,15 +720,21 @@ double-width floating point values have indices in the range // MapToIndexScale0 computes a bucket index at scale 0. func MapToIndexScale0(value float64) int32 { rawBits := math.Float64bits(value) + // rawExponent is a biased representation of the base-2 exponent: + // - value 0 indicates a subnormal representation or a zero value + // - value 2047 indicates an Inf or NaN value + // - value [1, 2046] are offset by ExponentBias (1023) rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth - rawSignificand := rawBits & SignificandMask + // rawFragment represents (significand-1) for normal numbers, + // where significand is in the range [1, 2). + rawFragment := rawBits & SignificandMask if rawExponent == 0 { - // Handle subnormal values: rawSignificand cannot be zero + // Handle subnormal values: rawFragment cannot be zero // unless value is zero. - rawExponent -= int64(bits.LeadingZeros64(rawSignificand) - 12) + rawExponent -= int64(bits.LeadingZeros64(rawFragment - 1) - 12) } ieeeExponent := int32(rawExponent - ExponentBias) - if rawSignificand == 0 { + if rawFragment == 0 { // Special case for power-of-two boundary: subtract one. return ieeeExponent - 1 } From 87577de5d81f1f8b956614badc43bee9644eba8a Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 27 Jul 2022 12:11:30 -0700 Subject: [PATCH 11/13] lengthen the explanation --- specification/metrics/data-model.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index b9135e2f87e..ce99098a581 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -720,22 +720,39 @@ double-width floating point values have indices in the range // MapToIndexScale0 computes a bucket index at scale 0. func MapToIndexScale0(value float64) int32 { rawBits := math.Float64bits(value) - // rawExponent is a biased representation of the base-2 exponent: + + // rawExponent is an 11-bit biased representation of the base-2 + // exponent: // - value 0 indicates a subnormal representation or a zero value // - value 2047 indicates an Inf or NaN value // - value [1, 2046] are offset by ExponentBias (1023) rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth + // rawFragment represents (significand-1) for normal numbers, // where significand is in the range [1, 2). rawFragment := rawBits & SignificandMask + + // Check for subnormal values: if rawExponent == 0 { // Handle subnormal values: rawFragment cannot be zero - // unless value is zero. + // unless value is zero. Subnormal values have up to 52 bits + // set, so for example greatest subnormal power of two, 0x1p-1023, has + // rawFragment = 0x8000000000000. Expressed in 64 bits, the value + // (rawFragment-1) = 0x0007ffffffffffff has 13 leading zeros. rawExponent -= int64(bits.LeadingZeros64(rawFragment - 1) - 12) + + // In the example with 0x1p-1023, the preceding expression subtracts + // (13-12)=1, leaving the rawExponent equal to -1. The next statement + // below subtracts `ExponentBias` (1023), leaving `ieeeExponent` equal + // to -1024, which is the correct upper-inclusive bucket index for + // the value 0x1p-1023. } ieeeExponent := int32(rawExponent - ExponentBias) + // Note that rawFragment and rawExponent cannot both be zero, + // or else the value is exactly zero, in which case the the ZeroCount + // bucket is used. if rawFragment == 0 { - // Special case for power-of-two boundary: subtract one. + // Special case for normal power-of-two values: subtract one. return ieeeExponent - 1 } return ieeeExponent From 68c75e237189d0af2c4d20a4485e94e949ede838 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 27 Jul 2022 12:55:53 -0700 Subject: [PATCH 12/13] lint --- specification/metrics/data-model.md | 39 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index ce99098a581..2d40acc5de4 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -35,7 +35,6 @@ linkTitle: Data Model - [Scale Zero: Extract the Exponent](#scale-zero-extract-the-exponent) - [Negative Scale: Extract and Shift the Exponent](#negative-scale-extract-and-shift-the-exponent) - [All Scales: Use the Logarithm Function](#all-scales-use-the-logarithm-function) - - [Positive Scale: Use a Lookup Table](#positive-scale-use-a-lookup-table) + [ExponentialHistogram: Producer Recommendations](#exponentialhistogram-producer-recommendations) + [ExponentialHistogram: Consumer Recommendations](#exponentialhistogram-consumer-recommendations) + [ExponentialHistogram: Bucket inclusivity](#exponentialhistogram-bucket-inclusivity) @@ -721,36 +720,36 @@ double-width floating point values have indices in the range func MapToIndexScale0(value float64) int32 { rawBits := math.Float64bits(value) - // rawExponent is an 11-bit biased representation of the base-2 - // exponent: - // - value 0 indicates a subnormal representation or a zero value - // - value 2047 indicates an Inf or NaN value - // - value [1, 2046] are offset by ExponentBias (1023) + // rawExponent is an 11-bit biased representation of the base-2 + // exponent: + // - value 0 indicates a subnormal representation or a zero value + // - value 2047 indicates an Inf or NaN value + // - value [1, 2046] are offset by ExponentBias (1023) rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth - // rawFragment represents (significand-1) for normal numbers, - // where significand is in the range [1, 2). + // rawFragment represents (significand-1) for normal numbers, + // where significand is in the range [1, 2). rawFragment := rawBits & SignificandMask // Check for subnormal values: if rawExponent == 0 { // Handle subnormal values: rawFragment cannot be zero // unless value is zero. Subnormal values have up to 52 bits - // set, so for example greatest subnormal power of two, 0x1p-1023, has - // rawFragment = 0x8000000000000. Expressed in 64 bits, the value - // (rawFragment-1) = 0x0007ffffffffffff has 13 leading zeros. + // set, so for example greatest subnormal power of two, 0x1p-1023, has + // rawFragment = 0x8000000000000. Expressed in 64 bits, the value + // (rawFragment-1) = 0x0007ffffffffffff has 13 leading zeros. rawExponent -= int64(bits.LeadingZeros64(rawFragment - 1) - 12) - // In the example with 0x1p-1023, the preceding expression subtracts - // (13-12)=1, leaving the rawExponent equal to -1. The next statement - // below subtracts `ExponentBias` (1023), leaving `ieeeExponent` equal - // to -1024, which is the correct upper-inclusive bucket index for - // the value 0x1p-1023. + // In the example with 0x1p-1023, the preceding expression subtracts + // (13-12)=1, leaving the rawExponent equal to -1. The next statement + // below subtracts `ExponentBias` (1023), leaving `ieeeExponent` equal + // to -1024, which is the correct upper-inclusive bucket index for + // the value 0x1p-1023. } ieeeExponent := int32(rawExponent - ExponentBias) - // Note that rawFragment and rawExponent cannot both be zero, - // or else the value is exactly zero, in which case the the ZeroCount - // bucket is used. + // Note that rawFragment and rawExponent cannot both be zero, + // or else the value is exactly zero, in which case the the ZeroCount + // bucket is used. if rawFragment == 0 { // Special case for normal power-of-two values: subtract one. return ieeeExponent - 1 @@ -834,7 +833,7 @@ bucket index by applying a scaling factor, derived as follows. 7. Subtract one from each side: `index == Ceiling(log_base(value)) - 1` 8. Rewrite using `log_X(Y) == log_N(Y) / log_N(X)` to allow use of the natural logarithm 9. Thus, `index == Ceiling(log(value)/log(base)) - 1` -12. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). +10. The scaling factor `1/log(base)` can be derived using the formulas in (1), (4), and (8). The scaling factor equals `2**scale / log(2)` can be written as `math.Ldexp(math.Log2E, scale)` since the constant `math.Log2E` is From b0014b0da301d4ef75a1385e70aaf5027501a5bb Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 28 Jul 2022 12:52:57 -0700 Subject: [PATCH 13/13] Update specification/metrics/data-model.md Co-authored-by: Reiley Yang --- specification/metrics/data-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/metrics/data-model.md b/specification/metrics/data-model.md index 2d40acc5de4..2a281a6bd77 100644 --- a/specification/metrics/data-model.md +++ b/specification/metrics/data-model.md @@ -862,7 +862,7 @@ apply such a special case: func MapToIndex(value float64) int { // Special case for power-of-two values. if frac, exp := math.Frexp(value); frac == 0.5 { - return ((exp-1) << scale) - 1 + return ((exp - 1) << scale) - 1 } scaleFactor := math.Ldexp(math.Log2E, scale) // Note: math.Floor(value) equals math.Ceil(value)-1 when value