[Haskell-beginners] Printing the entries of a Data.Map m

Daniel Fischer daniel.is.fischer at googlemail.com
Sun May 15 14:23:36 CEST 2011


On Sunday 15 May 2011 11:34:33, Matthias Guedemann wrote:
> Hi Manfred
> 
> > It didn't make much of a difference in runtime doing it without
> > toList. I'm not quite sure if it is really more efficient or not.
> > Perhaps I would have to create dictonary with some millions entries
> > in order to see a noticable difference?
> 
> Probably, Haskell is not very deterministic for memory / runtime
> forecasts :-)
> 
> In theory there should be no difference in complexity, both need one
> sweep over all entries of the Map. There could perhaps be a constant
> cost for constructing the list.

The call-tree for the list construction is similar to the call-tree that 
traverse builds.
However, traverse assembles a new Map. While it's trivial to discard 
consumed list-cells (if the list is consumed sequentially), that is not so 
for Maps. Unless the compiler manages to completely eliminate the new Map, 
that is going to cost considerably more than the throwaway list-cells.

> Nevertheless, laziness should make it
> possible to run in constant space, as neither the list, nor the
> transformed Map is used.

Well, you need the space for the initial Map, that is O(size).
I assume you mean that the additional memory needed is O(1).
That is almost true for going through the list, since that is swiftly 
collected. The call-tree to get at the elements, however, is O(log size), 
but that's practically constant size.
For traverse, that is only true if the new Map is completely discarded 
(Maps are spine-strict, so if it's not completely ignored, its full spine 
is built, requiring O(size) space).

> So differences are probably small.
> 
> But is possible, take some measurements and report, not just runtime,
> but also heap space usage.

Note that the "mapM_ print . toAscList" prints the (key, value) pairs while 
the traverse (or Data.Traversable.mapM) only prints the values. That makes 
a difference.

I put together a small (trivial) test:

viaList :: (Ord k, Show k, Show v) => Map k v -> IO ()
viaList = mapM_ print . toAscList

-- and the same using elems instead of toAscList to only print the values

perTraverse :: (Ord k, Show v) => Map k v -> IO ()
perTraverse mp = traverse print mp >> return ()

main :: IO ()
main = do
    args <- getArgs
    let sz = case args of
                (a:_) -> read a
                _ -> 100000
        mp :: Map Int Int
        mp = fromDistinctAscList [(i,2*i+1) | i <- [0 .. sz]]
    print $ size mp   -- force the spine
    perTraverse mp   -- or via list (pairs/values only)

Everything compiled with -O2 (output redirected to /dev/null), the results 
for input 1000000 are:

./useElems 1000000 +RTS -s 
   1,026,031,460 bytes allocated in the heap
     247,759,432 bytes copied during GC
      40,026,204 bytes maximum residency (8 sample(s))
         353,148 bytes maximum slop
             103 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  1869 collections,     0 parallel,  0.53s,  0.53s elapsed
  Generation 1:     8 collections,     0 parallel,  0.48s,  0.49s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    1.54s  (  1.55s elapsed)
  GC    time    1.01s  (  1.02s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    2.56s  (  2.56s elapsed)

  %GC time      39.6%  (39.7% elapsed)

  Alloc rate    664,729,202 bytes per MUT second

  Productivity  60.3% of total user, 60.1% of total elapsed
----------------------------------------
./useAscList 1000000 +RTS -s 
   1,337,743,996 bytes allocated in the heap
     247,972,620 bytes copied during GC
      40,026,204 bytes maximum residency (8 sample(s))
         413,304 bytes maximum slop
             103 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  2467 collections,     0 parallel,  0.53s,  0.53s elapsed
  Generation 1:     8 collections,     0 parallel,  0.45s,  0.45s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    2.23s  (  2.23s elapsed)
  GC    time    0.98s  (  0.99s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    3.22s  (  3.22s elapsed)

  %GC time      30.6%  (30.7% elapsed)

  Alloc rate    599,228,735 bytes per MUT second

  Productivity  69.3% of total user, 69.3% of total elapsed
----------------------------------------
./useTraverse 1000000 +RTS -s 
   1,266,460,972 bytes allocated in the heap
     568,553,592 bytes copied during GC
      61,013,024 bytes maximum residency (10 sample(s))
         614,608 bytes maximum slop
             159 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  2328 collections,     0 parallel,  1.32s,  1.32s elapsed
  Generation 1:    10 collections,     0 parallel,  1.18s,  1.18s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    1.82s  (  1.83s elapsed)
  GC    time    2.50s  (  2.50s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    4.32s  (  4.33s elapsed)

  %GC time      57.8%  (57.8% elapsed)

  Alloc rate    695,540,479 bytes per MUT second

  Productivity  42.1% of total user, 42.0% of total elapsed
----------------------------------------

Data.Traversable.traverse uses more memory. Not twice as much, since the 
original Map is disassembled while traversing it. If I force the original 
Map to stay intact, it's about twice.
I conclude that the new Map assembled by traverse is not discarded.
So, overall, going through the list is better.

Cheers,
Daniel



More information about the Beginners mailing list