[Haskell-beginners] The cost of generality, or how expensive is realToFrac?

Greg greglists at me.com
Wed Sep 15 14:50:13 EDT 2010

Hey, thanks, Daniel.

I hadn't come across rewrite rules yet.  They definitely look like something worth learning, though I'm not sure I'm prepared to start making custom versions of OpenGL.Raw...

It looks like I managed to put that battle off for another day, however.  I did look at how realToFrac is implemented and (as you mention) it does the fromRational . toRational transform pair suggested in a number of sources, including Real World Haskell.  Looking at what toRational is doing, creating a ratio of integers out of a float it seems like a crazy amount of effort to go through just to convert floating point numbers.

Looking at the RealFloat class rather that Real and Fractional, it seems like this is a much more efficient way to go:

floatToFloat :: (RealFloat a, RealFloat b) => a -> b
floatToFloat = (uncurry encodeFloat) . decodeFloat

I substituted this in for realToFrac and I'm back to close to my original performance.  Playing with a few test cases in ghci, it looks numerically equivalent to realToFrac.

This begs the question though-- am I doing something dangerous here?  Why isn't this the standard approach?  

If I understand what's happening, decodeFloat and encodeFloat are breaking the floating point numbers up into their constituent parts-- presumably by bit masking the raw binary.  That would explain the performance improvement.  I suppose there is some implementation dependence here, but as long as the encode and decode are implemented as a matched set then I think I'm good.


On Sep 15, 2010, at 1:56 AM, Daniel Fischer wrote:

> On Wednesday 15 September 2010 02:51:01, Greg wrote:
>> First, to anyone who recognizes me by name, thanks to the help I've been
>> getting here I've managed to put together a fairly complex set of code
>> files that all compile together nicely, run, and do exactly what I
>> wanted them to do.  Success!
>> The trouble is that my implementation is dog slow
>> Fortunately, this isn't the first time I've been in over my head and I
>> started by putting up some simpler scaffolding- which runs much more
>> quickly.  Working backwards, it looks like the real bottle neck is in
>> the data types I've created, the type variables I've introduced, and the
>> conversion code I needed to insert to make it all happy.
>> I'm not sure it helps, but I've attached a trimmed down version of the
>> relevant code.  What should be happening is my pair  is being converted
>> to the canonical form for Coord2D which is Cartesian2D and then
>> converted again to Vertex2.  There shouldn't be any change made to the
>> values, they're only being handed from one container to another in this
>> case (Polar coordinates would require computation, but I've stripped
>> that out for the time being).  However, those handoffs require calls to
>> realToFrac to make the type system happy, and that has to be what is
>> eating up all my CPU.
> Not all, but probably a big chunk of it.
> The problem is that the default implementation of realToFrac is
> realToFrac = fromRational . toRational
> a) with that implementation, realToFrac :: Double -> Double is not the 
> identity (doesn't respect NaNs)
> b) it's slow, there are no special operations to convert Double, Float etc. 
> from/to Rational.
> For a lot of types, GHC provides rewrite rules (you need to compile with 
> optimisations to have them fire) which give faster versions (with somewhat 
> different behaviour, e.g. realToFrac :: Double -> Double is rewritten to 
> id, realToFrac between Float and Double uses primitive widening/narrowing 
> ops, for several newtype wrappers around Float/Double there are rules too).
>> I think there are probably 4 calls to realToFrac.  If I walk through the
>>  code, the result, given the pair p, should be: Vertex2 (realToFrac
>> (realToFrac (fst p)))  (realToFrac (realToFrac (snd p)))
>> I'd like to maintain type independence if possible, but I expect most
>> uses of this code to feed Doubles in for processing and probably feed
>> GLclampf (Floats, I believe)
> newtype wrapper around CFloat, which is a newtype wrapper around Float
> Unfortunately, there are no rewrite rules in the module where it is 
> defined, apparently neither any other module that has access to the 
> constructor. And the constructor is not accessible from any of the exposed 
> modules, so as far as I know, you can't provide your own rewrite rules.
>> to the OpenGL layer.  If there's a way to
>> do so, I wouldn't mind optimizing for that particular set of types.
>>  I've tried GLdouble, and it doesn't really improve things with the
>> current code.
>> Is there a way to short circuit those realToFrac calls if we know the
>> input and output are the same type?  Is there a way merge the nested
>> calls?
> You can try rewrite rules
> {-# RULES
>   "realToFrac2/realToFrac"         realToFrac . realToFrac = realToFrac
>   "realToFrac/id"                  realToFrac = id
>   #-}
> but I'm afraid the second won't work at all, then you'd have to specify all 
> interesting cases yourself (there are rules for the cases Double -> Double 
> and Float -> Float in GHC.Float, rules for converting from/to CFloat and 
> CDouble in Foreign.C.Types, so those should be fine too)
>   "realToFrac/GLclampf->GLclampf"  realToFrac = id :: GLclampf -> GLclampf
> and what ese you need.
> Whether the first one will help (or even work), I don't know either, you 
> have to try.
>> Any other thoughts on what I can do here?  The slow down between the two
>> implementations is at least 20x, which seems like a steep penalty to
>> pay.
> In case of emergency, put the needed rewrite rules into the source of 
> OpenGLRaw yourself.
>> And while I'm at it, is turning on FlexibleInstances the only way to
>> create an instance for (a,a)?
> Yes. Haskell98 doesn't allow such instance declarations, so you need the 
> extension.

More information about the Beginners mailing list