Multiple updates in ST-Monad

146 views Asked by At

I want to learn using the ST-Monad. Therefore I want to rewrite a bit of code computing for every integer – up to a limit – the list of all its proper divisors. The result should be an array and the entry of index 'n' should be the list of it's proper divisors.

It is done by computing for each Integer 'n' a list 'l' of its multiples and adding for each multiple 'm' out of 'l' at the index 'm' it's divisor 'n' to the list.

Here's the code I want to modify:

properDivisorsOf' :: forall a. (Integral a, Ix a) => a -> Array a [a]
properDivisorsOf' limit =
  let generate :: (Integral a, Ix a) => a -> Array a [a] -> Array a [a]
      generate n acc
        | n > (limit `div` 2) = acc
        | otherwise           =
              let acc' = acc // [(i, n : (acc ! i)) | i <- [2*n, 3*n .. limit]]
              in  generate (n + 1) acc'

  in generate 1 (array (1, limit) [(i, [])| i <- [1..limit]])

And that's the way how I try it:

properDivisorsOf :: forall a. (Integral a, Ix a) => a -> Array a [a]
properDivisorsOf limit =
  let result :: ST s (STArray s a [a])
      result = newArray (1, limit) [] -- In the beginning for every number: no divisors known (empty list) 

      update (index, divisor) = do
        l <- readArray result index -- extracting list of divisors of number 'index'
        let u = divisor : l
        writeArray result index u   -- and adding 'divisor' to the list

      content :: [(a, a)]
      content = do
        n <- [1 .. (limit `div` 2)]
        multiple <- [2*n, 3*n .. limit]
        return (multiple, n)

      doUpdate = map update content -- update result for all multiples (in content)

in runSTArray result

Unfortunatley it is not compiling and the error message doesn't say anything to me. I have two questions:

  1. Why doesn't it compile? How can I extract the entry correctly?
  2. How would an experienced Haskell-Programm solve this problem under the restriction that he has to work with the ST-Monad (for efficiency purposes)

EDIT: Compiler message

    Couldn't match expected type `[t0]'
            with actual type `STArray i0 a [a]'
    In the second argument of `(:)', namely `l'
    In the expression: divisor : l
    In an equation for `u': u = divisor : l

Failed, modules loaded: none.

1

There are 1 answers

8
Zeta On BEST ANSWER

Solution

2. How would an experienced Haskell-Programm solve this problem under the restriction that he has to work with the ST-Monad (for efficiency purposes)?

I'm by no means a experienced Haskell programmer, but I would use the following code the imperative code below, but here's the direct transition from your code:

properDivisorsOf :: forall a. (Integral a, Ix a) => a -> Array a [a]
properDivisorsOf limit =
  runSTArray $ do
    result <- newArray (1, limit) []
    mapM_ (update result) content
    return result
  where
      content :: [(a, a)]
      content = do
        n <- [1 .. (limit `div` 2)]
        multiple <- [2*n, 3*n .. limit]
        return (multiple, n)

      update arr (index, divisor) = do
        l <- readArray arr index -- extracting list of divisors of number 'index'
        let u = divisor : l
        writeArray arr index u   -- and adding 'divisor' to the list

Comparing non-ST vs ST:

non-ST variant

Your original function:

main = print $ properDivisorsOf' 100000
$ .\Original.exe +RTS -s
Original.exe: out of memory

We exchange 100000 by 10000:

     221,476,488 bytes allocated in the heap
      21,566,328 bytes copied during GC
     172,813,068 bytes maximum residency (9 sample(s))
       4,434,480 bytes maximum slop
             210 MB total memory in use (5 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0       378 colls,     0 par    0.41s    0.43s     0.0011s    0.0024s
  Gen  1         9 colls,     0 par    0.36s    0.37s     0.0409s    0.1723s

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    0.28s  (  0.60s elapsed)
  GC      time    0.77s  (  0.80s elapsed)
  EXIT    time    0.00s  (  0.02s elapsed)
  Total   time    1.05s  (  1.42s elapsed)

  %GC     time      73.1%  (56.4% elapsed)

  Alloc rate    787,471,957 bytes per MUT second

  Productivity  26.9% of total user, 19.8% of total elapsed

Even though the program is pretty fast (1s after all), the memory footprint of 210MB is quite noticeable. Also, the memory needed grows squre, for a limit of 20000 you would need around 800 MB, for 20000 around 1.9GB.

ST variant

$ .\ST.exe +RTS -s > $null
     300,728,400 bytes allocated in the heap
      89,696,288 bytes copied during GC
      13,628,272 bytes maximum residency (10 sample(s))
         292,972 bytes maximum slop
              38 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0       565 colls,     0 par    0.14s    0.14s     0.0002s    0.0008s
  Gen  1        10 colls,     0 par    0.09s    0.08s     0.0076s    0.0223s

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    0.11s  (  0.16s elapsed)
  GC      time    0.23s  (  0.21s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    0.34s  (  0.38s elapsed)

  %GC     time      68.2%  (56.6% elapsed)

  Alloc rate    2,749,516,800 bytes per MUT second

  Productivity  31.8% of total user, 28.9% of total elapsed

Only 38 MB, which is ~17% of your original implementation, but calculates 10 times more values (10000 vs 100000).

Imperative variant

If you throw content away, you'll end up with the following program:

import Data.Array (Array(..), Ix)
import Data.Array.ST (newArray, readArray, writeArray, runSTArray)
import Control.Monad (forM_)

properDivisorsOf :: (Integral a, Ix a) => a -> Array a [a]
properDivisorsOf limit =
  runSTArray $ do
    result <- newArray (1, limit) []
    forM_ [1.. (limit `div`2)] $ \n -> do
      forM_ [2*n, 3*n .. limit] $ \index -> do
        l <- readArray result index
        writeArray result index (n:l)
    return result

main = print $ properDivisorsOf 100000
$ .\Imperative.exe +RTS -s > $null
     237,323,392 bytes allocated in the heap
      63,304,856 bytes copied during GC
      13,628,276 bytes maximum residency (7 sample(s))
         138,636 bytes maximum slop
              34 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0       447 colls,     0 par    0.12s    0.09s     0.0002s    0.0008s
  Gen  1         7 colls,     0 par    0.05s    0.06s     0.0087s    0.0224s

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    0.11s  (  0.18s elapsed)
  GC      time    0.17s  (  0.16s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    0.30s  (  0.34s elapsed)

  %GC     time      57.9%  (45.9% elapsed)

  Alloc rate    2,169,813,869 bytes per MUT second

  Productivity  42.1% of total user, 36.9% of total elapsed

Why doesn't it compile?

  1. Why doesn't it compile? How can I extract the entry correctly?

I a sense, you extract correctly (see above, that's almost the same code you used), but the inferred type for update is wrong, since your usage isn't correct. For example update should work in the ST monad, and therefore you would use it with mapM, not map. Also, there are some other things off, for example you define doUpdate but never use it.