Previous: Fixnum arithmetic, Up: Efficiency Tips
Getting efficient flonum arithmetic is much more complicated and harder than getting efficient fixnum arithmetic.
One of the main disadvantages of generic arithmetic is that not all kinds of number fit in a machine register. Flonums have to be boxed because a 64-bit IEEE floating-point number (the representation that MIT/GNU Scheme uses) does not fit in a regular machine word. This is true even on 64-bit architectures because some extra bits are needed to distinguish floating-point numbers from other objects like pairs and strings. Values are boxed by storing them in a small record in the heap. Every floating-point value that you see at the REPL is boxed. Floating-point values are unboxed only for short periods of time when they are in the machine's floating-point unit and actual floating-point operations are being performed.
Numerical calculations that happen to be using floating-point numbers cause many temporary floating-point numbers to be allocated. It is not uncommon for numerical programs to spend over half of their time creating and garbage collecting the boxed flonums.
Consider the following procedure for computing the distance of a point (x,y) from the origin.
(define (distance x y) (sqrt (+ (* x x) (* y y))))
The call `(distance 0.3 0.4)' returns a new, boxed flonum, 0.5. The calculation also generates three intermediate boxed flonums. This next version works only for flonum inputs, generates only one boxed flonum (the result) and runs eight times faster:
(define (flo:distance x y) (flo:sqrt (flo:+ (flo:* x x) (flo:* y y))))
Note that flo:
operations are usually effective only within a
single arithmetic expression. If the expression contains conditionals
or calls to procedures then the values tend to get boxed anyway.
Flonum vectors are vectors that contain only floating-point values, in much the same way as a string is a `vector' containing only character values.
Flonum vectors have the advantages of compact storage (about half that of a conventional vector of flonums) and judicious use of flonum vectors can decrease flonum consing.
The disadvantages are that flonum vectors are incompatible with ordinary vectors, and if not used carefully, can increase flonum consing. Flonum vectors are a pain to use because they require you to make a decision about the representation and stick with it, and it might not be easy to ascertain whether the advantages in one part of the program outweigh the disadvantages in another.
The flonum vector operations are:
Create a flonum vector of length n. The contents of the vector are arbitrary and might not be valid floating-point numbers. The contents should not be used until initialized.
These operations are analogous to the ordinary vector operations.
The following operation causes no flonum consing because the flonum is loaded directly from the flonum vector into a floating-point machine register, added, and stored again. There is no need for a temporary boxed flonum.
(flo:vector-set! v 0 (flo:+ (flo:vector-ref v 0) 1.2))
In this next example, every time g
is called, a new boxed flonum
has to be created so that a valid Scheme object can be returned. If
g
is called more often than the elements of v are changed
then an ordinary vector might be more efficient.
(define (g i) (flo:vector-ref v i))
Pitfall 1: Make sure that your literals are floating-point constants:
(define (f1 a) (flo:+ a 1)) (define (f2 a) (flo:+ a 1.))
f1
will most likely cause a hardware error, and certainly give
the wrong answer. f2
is correct.
Pitfall 2:
It is tempting to insert calls to exact->inexact
to coerce values
into flonums. This does not always work because complex numbers may be
exact or inexact too. Also, the current implementation of
exact->inexact
is slow.
Pitfall 3:
A great deal of care has to be taken with the standard math procedures.
For example, when called with a flonum, both sqrt
and asin
can return a complex number (e.g with argument -1.5
).