When we explore underflow in the code to come, we want a starting
point tiny but not too close to the underflow threshold. This
function returns the value intended to be assigned to C
.
We'll not forget that it's C
that's tiny, makeing
1/C
huge.
def compute_C_and_inverse():
"""Return untility constant C = 1 / B**k, safely above the underflow
threshold, and its reciprocal.
C is a useful starting point for investigation of underflow.
It has the form 1 / B**k with k chosen so that C is farther than
a factor of 1 / B**PRECISION of underflowing to zero.
C is a normal number. It's reciprocal is a large power of B.
Returns:
C, 1 / C
Basic 4370-4400
"""
First, we compute d
, typical just an ulp of
values less than \( 1 \). When the number of digits is
fractional, we expand the last digit to full size.
Here is the case for 22 bits in hexadecimal.
The value just computed is an ulp of numbers just less than \( 1 \) in six hex digits.
d = ULP_OF_ONE_MINUS # default value for integer precision
if PRECISION != floor(PRECISION):
# Compute 1 / B**ceiling(PRECISION)
d = ONE_OVER_B
for _ in range(floor(PRECISION)):
d = d * ONE_OVER_B
The next loop computes value c
that
is roughly the square root or perhaps the fourth root
of the underflow threshold.
This is one quick leap, before the loop that follows walks
to the underflow threshold in measured steps.
This code exhibits the Paranoia idiom for detecting underflow.
The typical step is to compute z = y * y
for
a tiny value y
. The two cases tested are
z + z <= z
which arises when z
underflows to zero or to an
epsilon value \epsilon
of nonzero but
indeterminate size that behaves well in comparisons
y <= z
which arises when underflow ultimately
pins at a nonzero value the compares reasonably with itself
Typically, it's the first case that terminates the loop.
In this case, c
is left pessimistically far
from the underfulow threshold, resulting in extra steps
in the next loop.
# d = 1 / B**k for k = PRECISION or floor(PRECISION)+1
# Compute 1/d * 1/d**2 * 1/d**4 * ... until the multiplier
# tapers off (due to underflow). This is the quick route to
# a very tiny 1 / B**k in ever-inceasing steps.
y = ONE
z = d
while True:
c = y
y = z
z = y * y
if y <= z or z + z <= z: break
The second loop approaches the underflow threshold step by
step by a factor of d
. In an IEEE 754 system,
z
underflows to zero, y
is possibly
subnormal, and c
is a tiny normal number.
In arithmetic that just flushes to zero past a certain point,
both y
and c
will be normal.
In any case, c
is tiny and normal.
# Finally, step toward the minimum normal number by multiples of d.
# c stays one step behind y, so that at termination
# 1 >> c > B**(min+PRECISION) > y > B**mnin > z (underflows)
y = c
z = y * d
while True:
c = y
y = z
z = y * d
if y <= z or z + z <= z: break
return c, ONE / c
Exercise: Compute c
for the case of six-digit decimal arithmetic in which any
value smaller than \( 10 ^ {-99} \) flushes to zero.
As a hint, an ulp of a number just less than one
is \( 10 ^ {-6} \).
Exercise: If you have some
familiarity with IEEE 754 arithmetic, compute c
for the single
format, with gradual underflow.
An ulp of a number just less than one
is \( 2 ^ {-24} \).