in reply to Re^2: Determining the minimum representable increment/decrement possible?
in thread Determining the minimum representable increment/decrement possible?

I'm sure you've moved beyond this... but for future searchers:

While trying to implement some ULP-manipulating routines in my Data::IEEE754::Tools development (which I used to call Expand.pm), I was googling to fix a bug, and was led to Data::Float, which actually has most of the tools I was trying to implement in my module. Using that CPAN module, I was able to write a sub which found the ULP for any finite value (normal or denormal); it returns INF or NAN when one of those was passed.

use warnings; use strict; use feature qw/say/; use Data::Float qw/pow2 float_parts float_hex significand_bits min_normal min_normal_exp min_finite min_finite_exp max_finite pos_zero neg_zero pos_infinity neg_infinity nan float_is_zero float_is_finite/; sub find_ulp($) { my $nv = shift; my $vh = float_hex($nv); if(!float_is_finite($nv)) { # INF or NAN say "ulp($nv:$vh) = $nv:$vh"; return $nv; } my $e = float_is_zero($nv) ? (min_finite_exp) : ( (float_parts($n +v))[1] - significand_bits ); my $uv = pow2($e); my $uh = float_hex($uv); say "ulp($nv:$vh) = $uv:$uh"; return $uv; } find_ulp($_) foreach (2.0, 1.0, 0.5, 0.16, max_finite, min_normal, min +_normal/2., min_finite, pos_zero, neg_zero , pos_infinity , neg_infin +ity , nan); __END__ ulp(2:+0x1.0000000000000p+1) = 4.44089209850063e-016:+0x1.000000000000 +0p-51 ulp(1:+0x1.0000000000000p+0) = 2.22044604925031e-016:+0x1.000000000000 +0p-52 ulp(0.5:+0x1.0000000000000p-1) = 1.11022302462516e-016:+0x1.0000000000 +000p-53 ulp(0.16:+0x1.47ae147ae147bp-3) = 2.77555756156289e-017:+0x1.000000000 +0000p-55 ulp(1.79769313486232e+308:+0x1.fffffffffffffp+1023) = 1.99584030953472 +e+292:+0x1.0000000000000p+971 ulp(2.2250738585072e-308:+0x1.0000000000000p-1022) = 4.94065645841247e +-324:+0x0.0000000000001p-1022 ulp(1.1125369292536e-308:+0x0.8000000000000p-1022) = 4.94065645841247e +-324:+0x0.0000000000001p-1022 ulp(4.94065645841247e-324:+0x0.0000000000001p-1022) = 4.94065645841247 +e-324:+0x0.0000000000001p-1022 ulp(0:+0.0) = 4.94065645841247e-324:+0x0.0000000000001p-1022 ulp(0:-0.0) = 4.94065645841247e-324:+0x0.0000000000001p-1022 ulp(Inf:+inf) = Inf:+inf ulp(-Inf:-inf) = -Inf:-inf ulp(NaN:nan) = NaN:nan
  • Comment on Re^3: Determining the minimum representable increment/decrement possible?
  • Download Code

Replies are listed 'Best First'.
Re^4: Determining the minimum representable increment/decrement possible?
by syphilis (Archbishop) on Jul 01, 2016 at 01:09 UTC
    Yes, Data::Float (which had slipped my mind at the time) is worth a mention here.
    It also provides nextup, nextdown and nextafter functions.

    Be aware that the outputs you're getting provide insufficient precision - eg the approximation 4.44089209850063e-016 instead of the accurate 4.4408920985006262e-16.

    Cheers,
    Rob
Re^4: Determining the minimum representable increment/decrement possible?
by BrowserUk (Patriarch) on Jul 04, 2016 at 14:21 UTC
    I was googling to fix a bug, and was led to Data::Float, which actually has most of the tools I was trying to implement in my module.

    You should not let that stop you. That module gets (mostly) the right answers, but boy does it ever go about it in a convoluted way; which makes it very slow even for pure perl code.

    For example, this compares his method of nextup() and nextdown() with a couple of trivial routines I threw together when exploring this earlier:

    C:\test>FPstuff.pl Took:9.723010063 seconds ## his Took:0.949742079 seconds ## mine

    And this compares his float_hex() routine with my asHex():

    C:\test>FPstuff.pl "my" variable $start masks earlier declaration in same scope at C:\tes +t\FPstuff.pl line 108. Took:9.740586996 seconds ## his Took:0.699841022 seconds ## mine

    His are more than a magnitude slower in both cases -- mostly because he uses convoluted and complicated numerical methods rather than simple and pragmatic bitwise methods -- and certainly for my OP problem of trying to tune some floating point optimisations, that would be a show stopper.

    Here is the code that produce the above timings -- is just a mish-mash of bits and pieces from other scripts thrown together to test and validate that module. If any of it is useful to you; feel free to use it:

    #! perl -slw use Config; use Inline C => Config => BUILD_NOISY => 1; use Inline C => <<'END_C', NAME => 'nextafter', CLEAN_AFTER_BUILD =>0 +; #include <math.h> double cNextAfter( double x, double y ) { return _nextafter( x, y ); } SV *cAsHex( double d ) { SV * ret = newSVpv( "123456789012345678901234", 24 ); SvCUR( ret ) = sprintf( SvPVX(ret), "% -24.13a", d ); return ret; } END_C use constant DEBUG => $DEBUG; use strict; no warnings "portable"; use Data::Float qw[ nextafter nextup nextdown float_hex ]; use Time::HiRes qw[ time ]; use constant { SIGNBIT => 0x8000_0000_0000_0000, EXPBITS => 0x7FF0_0000_0000_0000, SIGBITS => 0xF_FFFF_FFFF_FFFF, INDBITS => 0x8_0000_0000_0000, }; use constant { POS_QNAN_LST => 0b0_11111111111_1111_11111111_11111111_11111111 +_11111111_11111111_11111111, POS_QNAN_1ST => 0b0_11111111111_1000_00000000_00000000_00000000 +_00000000_00000000_00000000, POS_SNAN_LST => 0b0_11111111111_0111_11111111_11111111_11111111 +_11111111_11111111_11111111, POS_SNAN_1ST => 0b0_11111111111_0000_00000000_00000000_00000000 +_00000000_00000000_00000001, POS_INF => 0b0_11111111111_0000_00000000_00000000_00000000 +_00000000_00000000_00000000, POS_NORM_LST => 0b0_11111111110_1111_11111111_11111111_11111111 +_11111111_11111111_11111111, POS_NORM_1ST => 0b0_00000000001_0000_00000000_00000000_00000000 +_00000000_00000000_00000000, POS_DENORM_LST => 0b0_00000000000_1111_11111111_11111111_11111111 +_11111111_11111111_11111111, POS_DENORM_1ST => 0b0_00000000000_0000_00000000_00000000_00000000 +_00000000_00000000_00000001, POS_ZERO => 0b0_00000000000_0000_00000000_00000000_00000000 +_00000000_00000000_00000000, NEG_ZERO => 0b1_00000000000_0000_00000000_00000000_00000000 +_00000000_00000000_00000000, NEG_DENORM_1ST => 0b1_00000000000_0000_00000000_00000000_00000000 +_00000000_00000000_00000001, NEG_DENORM_LST => 0b1_00000000000_1111_11111111_11111111_11111111 +_11111111_11111111_11111111, NEG_NORM_1ST => 0b1_00000000001_0000_00000000_00000000_00000000 +_00000000_00000000_00000000, NEG_NORM_LST => 0b1_11111111110_1111_11111111_11111111_11111111 +_11111111_11111111_11111111, NEG_INF => 0b1_11111111111_0000_00000000_00000000_00000000 +_00000000_00000000_00000000, NEG_SNAN_1ST => 0b1_11111111111_0000_00000000_00000000_00000000 +_00000000_00000000_00000001, NEG_SNAN_LST => 0b1_11111111111_0111_11111111_11111111_11111111 +_11111111_11111111_11111111, NEG_IND => 0b1_11111111111_1000_00000000_00000000_00000000 +_00000000_00000000_00000000, NEG_QNAN_1ST => 0b1_11111111111_1000_00000000_00000000_00000000 +_00000000_00000000_00000001, NEG_QNAN_LST => 0b1_11111111111_1111_11111111_11111111_11111111 +_11111111_11111111_11111111, }; use enum qw[ X Y ]; sub assert{ return unless DEBUG; die sprintf "%s - %s(%d): Assertion failed.\n", caller() unless $_ +[0]; } sub asBits{ join '_', unpack 'A1 A11 A4 (A8)6', unpack 'B64', scalar r +everse pack 'd', $_[X] } sub asHex{ my $bin = unpack 'Q', pack 'd', $_[X]; return "-0x1.#IND000000000p+0" if $bin == NEG_IND; my $sign = ( $bin & SIGNBIT ) ? '-' : ' '; my $exp = ( ( $bin & EXPBITS ) >> 52 ); my $mant = $bin & SIGBITS; if( $exp == 2047 ) { return $sign . "0x1.#INF000000000p+0" unless $mant; return $sign . ( $mant < INDBITS ? "0x1.#SNAN00000000p+0" : "0 +x1.#QNAN00000000p+0" ); } $exp -= 1023; my $hid = $exp == -1023 ? ( ++$exp, 0 ) : 1; sprintf "%s0x%1u.%013xp%+d", $sign, $hid, $mant, $exp; } sub asDouble{ unpack 'd', pack 'Q', $_[X] } sub nextUp{ return $_[X] if $_[X] != $_[X]; my $i = unpack 'Q', pack 'd', $_[X]; return asDouble( NEG_NORM_LST ) if $i == NEG_INF; return $_[X] if ( $i & POS_INF ) == POS_INF; return asDouble( POS_DENORM_1ST ) if $i == NEG_ZERO; return asDouble( $_[X] < 0 ? --$i : ++$i ); } sub nextDown { - nextUp( - $_[X] ) } sub nextAfter{ return $_[Y] if $_[X] == $_[Y]; return $_[X] < $_[Y] ? nextUp( $_[X] ) : nextDown( $_[X] ); } srand( 1 ); my $start = time; for( 1 .. 100000 ) { my $rv = unpack 'd', pack 'VV', int( rand 65536 ), int( rand 65536 + ); my $hex = float_hex( $rv ); } printf "Took:%.9f seconds\n", time() - $start; srand( 1 ); my $start = time; for( 1 .. 100000 ) { my $rv = unpack 'd', pack 'VV', int( rand 65536 ), int( rand 65536 + ); my $hex = asHex( $rv ); } printf "Took:%.9f seconds\n", time() - $start; __END__ srand( 1 ); my $start = time; for( 1 .. 100000 ) { my $rv = unpack 'd', pack 'VV', int( rand 65536 ), int( rand 65536 + ); my $up = nextup( $rv ); my $dn = nextdown( $rv ); } printf "Took:%.9f seconds\n", time() - $start; srand( 1 ); my $start = time; for( 1 .. 100000 ) { my $rv = unpack 'd', pack 'VV', int( rand 65536 ), int( rand 65536 + ); my $up = nextUp( $rv ); my $dn = nextDown( $rv ); } printf "Took:%.9f seconds\n", time() - $start; __END__ for my $first ( 2e-300, 2e-200, 2e-100, 2e-10, 2e-1, 2e0, 2e1, 2e10, 2e100, 2e +200, 2e300, map asDouble( $_ ), NEG_SNAN_1ST, NEG_SNAN_LST, NEG_IND, NEG_QNAN_1ST, NEG_QNAN_LST, NEG_INF, NEG_NORM_LST, NEG_NORM_1ST, NEG_DENORM_LST, NEG_DENORM_1S +T, NEG_ZERO, POS_ZERO, POS_DENORM_1ST, POS_DENORM_LST, POS_NORM_1ST, POS_NORM_LST, POS_IN +F, POS_SNAN_1ST, POS_SNAN_LST, POS_QNAN_1ST, POS_QNAN_LST, ) { printf "%32.18g\t%-24s\t%-24s\n", $first, asHex( nextAfter( $first, asDouble( POS_INF ) ) ), asHex( nextafter( $first, asDouble( POS_INF ) ) ); printf "%32.18g\t%-24s\t%-24s\n", $first, asHex( nextAfter( $first, asDouble( NEG_INF ) ) ), asHex( nextafter( $first, asDouble( NEG_INF ) ) ); } __END__ srand( 1 ); my $start = time; for( 1 .. 100000 ) { my $rv = unpack 'd', pack 'VV', int( rand 65536 ), int( rand 65536 + ); my $hex = float_hex( $rv ); } printf "Took:%.9f seconds\n", time() - $start; srand( 1 ); my $start = time; for( 1 .. 100000 ) { my $rv = unpack 'd', pack 'VV', int( rand 65536 ), int( rand 65536 + ); my $hex = asHex( $rv ); } printf "Took:%.9f seconds\n", time() - $start; __END__ for my $first ( map asDouble( $_ ), NEG_INF, NEG_NORM_LST, NEG_NORM_1ST, NEG_DENORM_LST, NEG_DENORM_1S +T, NEG_ZERO, POS_ZERO, POS_DENORM_1ST, POS_DENORM_LST, POS_NORM_1ST, POS_NORM_L +ST, POS_INF, ) { printf "nextDown(%s) % 32.17e\n (%s) % 32.17e\n nextUp(%s) + % 32.17e\n\n", asBits( nextDown( $first ) ), nextDown( $first ), asBits( $first ), $first, asBits( nextUp( $first ) ), nextUp( $first ); } for my $first ( 2e-300, 2e-200, 2e-100, 2e-10, 2e-1, 2e0, 2e1, 2e10, 2e100, 2e +200, 2e300, map asDouble( $_ ), NEG_SNAN_1ST, NEG_SNAN_LST, NEG_IND, NEG_QNAN_1ST, NEG_QNAN_LST, NEG_INF, NEG_NORM_LST, NEG_NORM_1ST, NEG_DENORM_LST, NEG_DENORM_1S +T, NEG_ZERO, POS_ZERO, POS_DENORM_1ST, POS_DENORM_LST, POS_NORM_1ST, POS_NORM_LST, POS_IN +F, POS_SNAN_1ST, POS_SNAN_LST, POS_QNAN_1ST, POS_QNAN_LST, ) { printf "%s\t%32.18g\t%s\t%s\n", asBits( $first ), $first, cAsHex( +$first ), asHex( $first ); }

    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority". I knew I was on the right track :)
    In the absence of evidence, opinion is indistinguishable from prejudice. Not understood.
    .

      I was asked off-line to explain what I meant by:

      he uses convoluted and complicated numerical methods rather than simple and pragmatic bitwise methods

      Which would you rather maintain or debug? My routine for formating a double into a hexadecimal floating point constant:

      use constant { SIGNBIT => 0x8000_0000_0000_0000, EXPBITS => 0x7FF0_0000_0000_0000, SIGBITS => 0xF_FFFF_FFFF_FFFF, INDBITS => 0x8_0000_0000_0000, }; sub asHex{ my $bin = unpack 'Q', pack 'd', $_[X]; return "-0x1.#IND000000000p+0" if $bin == NEG_IND; my $sign = ( $bin & SIGNBIT ) ? '-' : ' '; my $exp = ( ( $bin & EXPBITS ) >> 52 ); my $mant = $bin & SIGBITS; if( $exp == 2047 ) { return $sign . "0x1.#INF000000000p+0" unless $mant; return $sign . ( $mant < INDBITS ? "0x1.#SNAN00000000p+0" : "0 +x1.#QNAN00000000p+0" ); } $exp -= 1023; my $hid = $exp == -1023 ? ( ++$exp, 0 ) : 1; sprintf "%s0x%1u.%013xp%+d", $sign, $hid, $mant, $exp; }

      Or same thing from Data::Float:

      my %float_hex_defaults = ( infinite_string => "inf", nan_string => "nan", exp_neg_sign => "-", exp_pos_sign => "+", pos_sign => "+", neg_sign => "-", hex_prefix_string => "0x", subnormal_strategy => "SUBNORMAL", zero_strategy => "STRING=0.0", frac_digits => 0, frac_digits_bits_mod => "ATLEAST", frac_digits_value_mod => "ATLEAST", exp_digits => 0, exp_digits_range_mod => "IGNORE", ); sub _float_hex_option($$) { my($options, $name) = @_; my $val = defined($options) ? $options->{$name} : undef; return defined($val) ? $val : $float_hex_defaults{$name}; } use constant exp_digits_range => do { my $minexp = min_normal_exp - significand_bits; my $maxexp = max_finite_exp + 1; my $len_minexp = length(-$minexp); my $len_maxexp = length($maxexp); $len_minexp > $len_maxexp ? $len_minexp : $len_maxexp; }; use constant frac_digits_bits => (significand_bits + 3) >> 2; use constant frac_sections => do { use integer; (frac_digits_bits + 6) + / 7; }; sub float_hex($;$) { my($val, $options) = @_; return _float_hex_option($options, "nan_string") if $val != $val; if(have_infinite) { my $inf_sign; if($val == $pos_infinity) { $inf_sign = _float_hex_option($options, "pos_sign"); EMIT_INFINITY: return $inf_sign. _float_hex_option($options, "infinite_string"); } elsif($val == $neg_infinity) { $inf_sign = _float_hex_option($options, "neg_sign"); goto EMIT_INFINITY; } } my($sign, $exp, $sgnf); if($val == 0.0) { $sign = float_sign($val); my $strat = _float_hex_option($options, "zero_strategy"); if($strat =~ /\ASTRING=(.*)\z/s) { my $string = $1; return _float_hex_option($options, $sign eq "-" ? "neg_sign" : "pos_sign"). $string; } elsif($strat eq "SUBNORMAL") { $exp = min_normal_exp; } elsif($strat =~ /\AEXPONENT=([-+]?[0-9]+)\z/) { $exp = $1; } else { croak "unrecognised zero strategy `$strat'"; } $sgnf = 0.0; } else { ($sign, $exp, $sgnf) = float_parts($val); } my $digits = int($sgnf); if($digits eq "0" && $sgnf != 0.0) { my $strat = _float_hex_option($options, "subnormal_strategy"); if($strat eq "NORMAL") { my $add_exp; (undef, $add_exp, $sgnf) = float_parts($sgnf); $exp += $add_exp; $digits = "1"; } elsif($strat eq "SUBNORMAL") { # do nothing extra } else { croak "unrecognised subnormal strategy `$strat'"; } } $sgnf -= $digits; for(my $i = frac_sections; $i--; ) { $sgnf *= 268435456.0; my $section = int($sgnf); $digits .= sprintf("%07x", $section); $sgnf -= $section; } $digits =~ s/(.)0+\z/$1/; my $ndigits = 1 + _float_hex_option($options, "frac_digits"); croak "negative number of digits requested" if $ndigits <= 0; my $mindigits = 1; my $maxdigits = $ndigits + frac_digits_bits; foreach my $constraint (["frac_digits_bits_mod", 1+frac_digits_bit +s], ["frac_digits_value_mod", length($digits)]) { my($optname, $number) = @$constraint; my $mod = _float_hex_option($options, $optname); if($mod =~ /\A(?:ATLEAST|EXACTLY)\z/) { $mindigits = $number if $mindigits < $number; } if($mod =~ /\A(?:ATMOST|EXACTLY)\z/) { $maxdigits = $number if $maxdigits > $number; } croak "unrecognised length modification setting `$mod'" unless $mod =~ /\A(?:AT(?:MO|LEA)ST|EXACTLY|IGNORE)\z/; } croak "incompatible length constraints" if $maxdigits < $mindigits +; $ndigits = $ndigits < $mindigits ? $mindigits : $ndigits > $maxdigits ? $maxdigits : $ndigits; if($ndigits > length($digits)) { $digits .= "0" x ($ndigits - length($digits)); } elsif($ndigits < length($digits)) { my $chopped = substr($digits, $ndigits, length($digits), ""); if($chopped =~ /\A[89abcdef]/ && !($chopped =~ /\A80*\z/ && $digits =~ /[02468ace]\z/)) { for(my $i = length($digits) - 1; ; ) { my $d = substr($digits, $i, 1); $d =~ tr/0-9a-f/1-9a-f0/; substr($digits, $i, 1, $d); last unless $d eq "0"; } if($digits =~ /\A2/) { $exp++; substr($digits, 0, 1, "1"); } } } my $nexpdigits = _float_hex_option($options, "exp_digits"); my $mod = _float_hex_option($options, "exp_digits_range_mod"); if($mod eq "ATLEAST") { $nexpdigits = exp_digits_range if $nexpdigits < exp_digits_range; } elsif($mod ne "IGNORE") { croak "unrecognised exponent length ". "modification setting `$mod'"; } $digits =~ s/\A(.)(.)/$1.$2/; return sprintf("%s%s%sp%s%0*d", _float_hex_option($options, $sign eq "-" ? "neg_sign" : "pos_sign"), _float_hex_option($options, "hex_prefix_string"), $digits, _float_hex_option($options, $exp < 0 ? "exp_neg_sign" : "exp_pos_sign"), $nexpdigits, abs($exp)); }

      With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
      Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
      "Science is about questioning the status quo. Questioning authority". I knew I was on the right track :)
      In the absence of evidence, opinion is indistinguishable from prejudice. Not understood.