Keith Briggs

This page was last modified 2006 Jan 20  

.
.


home 
·papers 
·thesis 
·talks 
·meetings 
·records 
·maths notes 
·software « 
·languages 
·music 
·travel 
·cv 
·maps 
·people 
·photos 
·train data 
·links 


.

mpfl - an experiment in stochastic lazy floating-point arithmetic

What approaches are available when we need to do reliable floating-point arithmetic? I'm not thinking here of problems like "compute 100 million decimal digits of pi". What use are all those digits? And anyway, in such a calculation it is impossible in principle to guarantee that all digits are correct. Rather, I'm thinking of problems of the type "determine rigorously whether x<y or not", where x and y are computed numbers. Such a calculation can in principle be solved by a computation which terminates in finite time, assuming x and y are not equal. Problems of this type occur in computational number theory and computational geometry. Related problems include computing the floor of a computed real, and the continued fraction of an irrational (which needs the floor function).

Here is a typical approach, such as has traditionally been used in computer algebra systems, for getting results which are correct with (usually) high probability:

  • Set a precision level for the whole computation. Record the result. Repeat the whole computation at a higher precision. If the result doesn't change, it's probably correct.

It's clear that the computational complexity will increase as we move from demanding a result that is probably correct, to one that is guaranteed correct. The main question in this field is: can we write a general-purpose software package which gives guaranteed results with an acceptably short computation time? Here are some approaches:

  • Error analysis: perform (by hand) a complete analysis of the propagation of round-off error through the computation. Set the precision level of each step so that the final error is acceptably small. This method was used by some entries in the Many-digits friendly competition. But this is hardly feasible for a general-purpose software package.
  • Exact real arithmetic: there have been many attempts at this - see the links at xrc. Essentially, these methods use lazy evaluation where the whole directed acyclic graph (DAG) representing the computation is stored, and intermediate results are re-evaluated as needed. Each step is performed with a bounded error, so the final error is bounded. The main problems in practice concern the storage required to store the DAG, and the fact the error bounds are typically too pessimistic. In many cases, the minimum possible "graininess" of 1 bit (the smallest possible increase in precision per step) is larger than is actually needed to satisfy error bounds. In large calculations, this can result in unacceptable inefficiencies. Also, transcendental functions are difficult to make efficient in these methods.
  • iRRAM
  • RealLib

Experience shows that some of these approaches perform better on some types of problems, but no one method is best overall.

mpfl - the idea

The present method is an experimental stochastic variation of exact real arithmetic. The novelty is a way to avoid the 1-bit graininess mentioned above. It is assumed that arbitrary precision floating-point arithmetic with correct rounding is available (such as provided by mpfr). The ingredients are:

  • The complete DAG is stored, as in my xrc.
  • The intermediate value at each node is an interval with floating-point endpoints. These always strictly bound the correct result, but the intervals can be refined automatically as necessary.
  • When evaluation is initiated, the increase in precision as we move away from the output node is probabilistic, controlled by a parameter alpha. The optimum value (i.e. that minimizing computation time) for alpha will depend on the problem, but the final results are independent of alpha. In essence, if alpha<1, then 1 bit is added with probability alpha. If alpha>1, then floor(alpha) bits are added. This allows tuning - for example, for the logistic map problem, we know that alpha=1 should be optimal, because the Liapunov exponent is such that one bit is lost on average per iteration.
  • The results of evaluation-initiating functions like mpfl_cmp, mpfl_floor are guaranteed correct (subject to there being no bugs!).
  • mpfl_out_str tries to print the requested number of digits correctly, but does not guarantee correctness.

The stochastic component is of course a heuristic. Why might this be reasonable? Why not compute errors bounds exactly, as iRRAM does? One answer is already given above - error bounds are always pessimistic. Another answer is that computing error bounds itself takes time. One might compare the way packet protocols are implemented - at the lowest level, packets are just transmitted, regardless of whether they might suffer collisions or otherwise get lost. So at this level the system is stochastic, and transmission is not reliable. It is made reliable by being wrapped in another layer (e.g. TCP), which detects when packets are lost and resends them if necessary. My mpfl software has a similar two-level design - the lower level is not reliable, but the upper layer (the only layer with which the user interacts), is reliable. (More accurately, the lower layer actually is reliable, but not necessarily precise enough. The upper layer ensures sufficient precision.)

mpfl - download

not yet available

mpfl - installation

  • Make sure you have GMP, mpfr, and mpfi installed.
  • Edit the top few lines of Makefile and tests/Makefile.
  • make
  • make check

mpfl - available functions


const char* mpfl_get_version(void);

void mpfl_set_alpha(const double);
double mpfl_get_alpha(void);

mpfl_t mpfl_const_pi(void);

mpfl_t mpfl_set_d(const double);

mpfl_t mpfl_abs(const mpfl_t);
mpfl_t mpfl_neg(const mpfl_t);
mpfl_t mpfl_sqrt(const mpfl_t);
mpfl_t mpfl_exp(const mpfl_t);
mpfl_t mpfl_expm1(const mpfl_t);
mpfl_t mpfl_log(const mpfl_t);
mpfl_t mpfl_log1p(const mpfl_t);
mpfl_t mpfl_sin(const mpfl_t);
mpfl_t mpfl_cos(const mpfl_t);
mpfl_t mpfl_tan(const mpfl_t);
mpfl_t mpfl_atan(const mpfl_t);

mpfl_t mpfl_add(const mpfl_t, const mpfl_t);
mpfl_t mpfl_sub(const mpfl_t, const mpfl_t);
mpfl_t mpfl_mul(const mpfl_t, const mpfl_t);
mpfl_t mpfl_div(const mpfl_t, const mpfl_t);

mpfl_t mpfl_d_mul(const double, const mpfl_t);
mpfl_t mpfl_d_sub(const double, const mpfl_t);

mpfl_t mpfl_add_d(const mpfl_t, const double);
mpfl_t mpfl_sub_d(const mpfl_t, const double);
mpfl_t mpfl_mul_d(const mpfl_t, const double);
mpfl_t mpfl_div_d(const mpfl_t, const double);

int mpfl_cmp_d(mpfl_t x, const double d);
int mpfl_cmp(mpfl_t x, mpfl_t y);
double mpfl_floor(mpz_t y, mpfl_t x);

size_t mpfl_out_str(FILE *stream, int base, size_t digits, mpfl_t x);

The design is such that it is easy to add new functions:

  • Add a new opcode to "enum op", and a prototype, in mpfl.h
  • Add a new _CASE to the switch in eval in mpfr.c using the appropriate cpp macro
  • Add a new _OP function to mpfr.c using the appropriate cpp macro

At the moment there is no way to free mpfl_t objects. This will be added in a later version, using reference counting.

mpfl - example program

Coding is similar to xrc, but differs from mpfr in that variables do not have to be initialized, and functions return a pointer to their output, rather than return the result in a argument. This example is the classic problem: determine whether exp(pi*sqrt(163)) is integral or not. Here is how the code might look with different approaches.

  • double: (result inconclusive)
    
    #include <stdio.h>
    #include <math.h>
    int main() {
      double x=exp(M_PI*sqrt(163));
      printf("x-floor(x)=%g\n",x-floor(x));
      return 0;
    }
    
  • mpfr:
    
    #include <mpfr.h>
    int main() {
      mpfr_t x,pi,f;
      mpfr_set_default_prec(500);
      mpfr_init_set_ui(x,163);
      mpfr_init(pi);
      mpfr_init(f);
      mpfr_const_pi(pi);
      mpfr_sqrt(x,x);
      mpfr_mul(x,pi,x);
      mpfr_exp(x,x);
      mpfr_floor(f,x);
      mpfr_sub(x,x,f);
      printf("x-floor(x)>0?: %d\n",mpfr_cmp_ui(x,0));
      return 0;
    }
    
  • xrc:
    
    #include <xr.h>
    int main() {
      xr_t x,z;
      x=xr_exp(xr_mul(xr_pi(),xr_sqrt(xr_init(163,1))));
      z=xr_near_int(x); 
      printf("cmp(x,nearint(x))=%d\n",xr_cmp(x,z));
      return 0;
    }
    
  • mpfl:
    
    #include <mpfl.h>
    int main() {
      mpfl_t x;
      x=mpfl_exp(mpfl_mul(mpfl_const_pi(),mpfl_sqrt(mpfl_set_d(163))));
      printf("cmp(floor(x),x)=%d\n",mpfl_cmp(mpfl_floor(x),x));
      return 0;
    }
    
This page was last modified 2006 Jan 20 (Friday) 12:26 by Keith Briggs