[racket-dev] What are single flonums good for?

From: Vincent St-Amour (stamourv at ccs.neu.edu)
Date: Wed Sep 12 12:24:24 EDT 2012

Single-precision float support used to be enabled via a configure
option, which meant that some Racket installations would support them,
and some would not.

Since zo files are meant to be portable, they could not contain
single-precision floats. So, compilation would promote single literals
to doubles.

This meant that compilation could change the behavior of a program.
Here's an example:

    stamourv at ahuntsic:small-float-test$ cat test2.rkt
    #lang racket
    (define (f x) (displayln (flonum? x)))
    (f (if (with-input-from-string "#t" read) 1.0f0 2.0f0))
    stamourv at ahuntsic:small-float-test$ racket test2.rkt
    #f
    stamourv at ahuntsic:small-float-test$ raco make test2.rkt
    stamourv at ahuntsic:small-float-test$ racket test2.rkt
    #t
    stamourv at ahuntsic:small-float-test$

This example has to be a bit convoluted to defeat constant folding,
which makes the problem disappear:

    stamourv at ahuntsic:small-float-test$ cat test.rkt
    #lang racket
    (flonum? 1.0f0)
    stamourv at ahuntsic:small-float-test$ racket test.rkt
    #f
    stamourv at ahuntsic:small-float-test$ raco make test.rkt
    stamourv at ahuntsic:small-float-test$ racket test.rkt
    #f
    stamourv at ahuntsic:small-float-test$ raco decompile test.rkt
    (begin
      (module test ....
        (#%apply-values |_print-values@(lib "racket/private/modbeg.rkt")| '#f)))
    stamourv at ahuntsic:small-float-test$

This can make for pretty elusive bugs. This gets especially problematic
when you add unsafe float operations to the mix, which can turn these
issues into segfaults.

The solution we picked was to support single-precision floats by default
and to allow them in zos, which makes these discrepancies go away.

I agree that having to handle single floats when reasoning about numbers
complicates things, and it annoys me too. But I still think it's less
problematic than what I describe above.


At Wed, 12 Sep 2012 08:31:29 -0600,
Neil Toronto wrote:
> 
> I ask because I'm tired of worrying about them. More precisely, I'm 
> tired of Typed Racket (rightly) making me worry about them.
> 
> I'm also a little bit tired of this:
> 
> (: foo (case-> (Single-Flonum -> Single-Flonum)
>                 (Flonum -> Flonum)
>                 (Real -> Real)))
> (define (foo x)
>    (cond [(double-flonum? x)  (flfoo x)]
>          [(single-flonum? x)  (real->single-flonum
>                                (flfoo (real->double-flonum x)))]
>          [else  (flfoo (real->double-flonum x))]))

This function already converts rationals to doubles, and it seems
`flfoo' produces doubles too. You could drop the second clause, always
produce doubles, change the type to `(Real -> Flonum)' and leave the
conversion to single to the client. Since the math library always
operates on doubles internally anyway, this would also eliminate
unnecessary conversions to singles between stages of a pipeline.

> They make it easy to write wrong code, because it's easy to use 
> `exact->inexact' when you really should use `real->double-flonum'. Plot, 
> for example, fails to render functions that return single flonums. I'm 
> surprised it doesn't segfault.

Good point, that's a problem.

> I'm sure plot isn't the only one. Every use of `exact->inexact' in the 
> standard library is suspect. If followed by applying an unsafe op, it's 
> wrong.
> 
> Why do we have these things?

I don't know why they were added originally (as an option). In my limited
experience, I don't think I've seen non-test code that uses them.

Vincent

Posted on the dev mailing list.