views:

194

answers:

3

Hi guys!

I'm trying to create a small module for doing decimal-based calculations. A number is stored as an integer mantisse, with a precision value specified by an int:

data APNum =
  { getMantisse :: Integer
  , getPrecision :: Int }

For instance:

APNum 123 0 -> 123
APNum 123 1 -> 1.23
APNum 123 2 -> 12.3
...

(negative precision is not allowed).

Now I wrote this function, which adjusts the precision automatically by stripping as many trailing zero's as possible:

autoPrecision :: APNum -> APNum
  autoPrecision x@(APNum m p) = if p > maxPrecision
    then autoPrecision $ setPrecision x maxPrecision
    else autoPrecision' m p where
    autoPrecision' m p = let (m',r) = m `divMod` 10 in
      if r /= 0 || p <= 0 then APNum m p else autoPrecision' m' (pred p)

(MaxPrecision and setPrecision are obvious, I think).

The problem is, this snippet has a very bad performance, specially n numbers with more then 10000 digits. Are there any simple optimizations?

+3  A: 

You can use binary search to find the highest power of 10 which divides m, instead of trying all consecutive values.

import Numeric.Search.Range
import Data.Maybe

data APNum = APNum{getMantisse :: Integer, getPrecission :: Int} deriving Show

setPrecision (APNum m _) x = APNum m x
maxPrecission = 200000

findDiv x = pred $ fromJust $ searchFromTo (p x) 0 maxPrecission where
    p x n = x `mod` 10^n /= 0

autoPrecision :: APNum -> APNum
autoPrecision x@(APNum m p)
= if p > maxPrecission then
    autoPrecision $ setPrecision x maxPrecission else APNum m' p'
where d = min (findDiv m) p
        p' = p - d
        m' = m `div` 10^d

I'm using the binary-search package here which provides searchFromTo :: Integral a => (a -> Bool) -> a -> a -> Maybe a. This should give you a big speedup.

Daniel Velkov
BTW... Is there some laziness one can turn off in order to make this faster?
FUZxxl
This works as expected and cuts the execution time. The real problem is, that haskell consumes large amount of space while performing this. I'm going to write a full-program-example soon.
FUZxxl
A: 

also x mod 10 == 0 implies x mod 2 == 0, and that is cheaper to test for

Lars Lundgren
So are all numbers divisible by 2 divisble by 10, too? Think again :-)
Landei
No, but you can first divide by two (very cheap) and then, if n = 0 mod 2 divide by 5.
FUZxxl
+1  A: 

Looks like even straightforward string operation is still faster:

maxPrecision = 2000000

autoPrecision (APNum m p) =
   let p' = min p maxPrecision
       (n',ds) =  genericDropNWhile (=='0') p' $ reverse $ show m
   in APNum (read $ reverse ds) n'
   where
     genericDropNWhile p n (x:xs) | n > 0 && p x = genericDropNWhile p (n-1) xs
     genericDropNWhile _ n xs = (n,xs)

Test with this:

main = print $ autoPrecision $ APNum (10^100000) (100000-3)

EDIT: Oops, faster only for numbers with lots of zeroes. Otherwise this double conversion is definitely slower.

Ed'ka
I'm not sure that the cost of show and read would be worth it here. These will both have to do a lot of math operations to convert to/from a string.
Chris Smith
Well, on my machine my "main" function (see above) runs faster than the other two variants of autoPrecision (0.05s against 0.84s and 4.05s respectively). But, as I've noted, it's the case only when mantissa contains lots of trailing zeroes. I think this is because repeated application of `div` on large Integer is quite expensive while `show` I believe is cheaper here (need to check `show` implementation). I am not saying one should use show for this kind of things but rather that naive `div` is probably not the most optimal solution here.
Ed'ka