[GHC] #650: Improve interaction between mutable arrays and GC

GHC trac at galois.com
Wed Jan 11 06:21:00 EST 2006


#650: Improve interaction between mutable arrays and GC
-------------------------------+--------------------------------------------
    Reporter:  simonmar        |        Owner:                  
        Type:  task            |       Status:  new             
    Priority:  normal          |    Milestone:  6.6             
   Component:  Runtime System  |      Version:  6.4.1           
    Severity:  normal          |     Keywords:                  
          Os:  Unknown         |   Difficulty:  Moderate (1 day)
Architecture:  Unknown         |  
-------------------------------+--------------------------------------------
This is the result of a discussion between myself and Simon PJ a few weeks
 ago, inspired by recent discoveries of poor performance with mutable
 arrays (eg. see Data.Hash discussion on glasgow-haskell-users around
 October 2005).

 Note that all this applies to mutable arrays of pointers, i.e. `IOArray`
 and `STArray`, ''not'' to unboxed arrays, i.e. `IOUArray` and `STUArray`.

 === Current implementation ===

 There are two primitive types: `MutArray#` and `Array#`. We convert
 between them with:
 {{{
    unsafeFreezeArray# :: MutArr# s a -> State# s -> (# State# s, Array# a
 #)
    unsafeThawArray# :: Array# a -> State# s -> (# State# s, MutArr# s a #)
 }}}

 An Arr# is not normally on the old-gen mutable list, unless (a) it has
 pointers to young gen objects, or (b) it has been recently frozen.  The
 implementation of `unsafeFreezeArray#` is a single write to the header
 word of the array.  The implementation of `unsafeThawArray#` is slightly
 more complex: if the array was not already on the mutable list (indicated
 by the value of the header), then we add it.  Also, we change the header
 word to indicate that the array is now mutable.

 A `MutArr#` is ''always'' on the mutable list.

 Objects pointed to by Array# are ''eagerly promoted'' to the generation in
 which the Array# resides, with the aim that the Array# can then be removed
 from the mutable list.

 It is only safe to write to a `MutArr#`, so if multiple threads are
 accessing an array, they should ''not'' be doing thaw/freeze tricks
 without extra locking around the array (such behaviour can cause the GC to
 crash).

 === The Problem ===

 The problem is that mutable arrays are always completely traversed on
 every GC.  To get around this, we can keep an array in a frozen state and
 thaw it just before writing, then freeze it again afterward.  This is a
 bit inconvenient, not to mention unsafe with multiple threads unless extra
 locking is used.

 Furthermore, a modified array is ''completely'' scanned, whereas for
 larger arrays it would be much better to just scan the part of the array
 that had been modified (known in the GC literature as "card-marking").

 The benefit of the current approach is that writing to a mutable array is
 a single write instruction, whereas to do card-marking or something else
 requires a write-barrier.  The unsafeThaw/write/unsafeFreeze sequence
 amounts to a write barrier, so if this is a common technique we should
 provide an easy way to do it, possibly making it the default.

 === Solutions ===

 Leaving aside card-marking for now, let's think about incorporating the
 write barrier in the write operation.

 Suppose that mutable arrays are ''always'' kept on the mutable list, but
 the header word indicates whether the array needs to be scanned or not
 (eg. we have `MUT_ARR_DIRTY`, `MUT_ARR_CLEAN`).  The array write op should
 (a) set the header to `MUT_ARR_DIRTY`, and (b) do the write.  The GC turns
 `MUT_ARR_DIRTY` into `MUT_ARR_CLEAN` when everything the array points to
 is in the same generation (or older).

 Downsides to this:

   * intitialising a mutable array, or doing block writes, will be
     more painful, because each write will have the write barrier
     (perhaps not too painful)

 How does freezing/thawing interact with this?  We currently create
 immutable arrays by starting with a `MutArr#`, intitialising it, and then
 freezing it to make an `Arr#`.  We can still do this, exactly as now (and
 with the same thread-unsafety), but initialization will be a bit slower
 due to the write barrier.

 === Block writes ===

 We could try to provide for "block writes", by allowing a thread to "open"
 the array for modification, and then "close" it again after it had
 finished writing, with all writes in between being done without a write
 barrier.  This would replace unsafeThaw/unsafeFreeze.

 To do this safely, we would have to use some kind of synchronisation on
 the open/close; techniques that we came up with were to increment
 (atomically) a counter in the array header, or to allocate a new heap
 object pointing to the array in the current thread's allocation area.

 === Card marking ===

 We could refine the write barrier so that it marks just part of the array
 as dirty, instead of the whole array.  The natural choice is to put the
 mark bit in the block descriptor for the current block, giving us a
 granularity of 4/8k, which is possibly a bit large but other solutions are
 much more expensive.  Even this would significantly increase the cost of
 the write barrier, so it may be that we want a different kind of array
 type for this (`LargeMutArr#`?).  Furthermore, currently not all arrays
 have their own block descriptors ("large objects" in GHC's storage
 manager), the small ones are allocated in movable memory.  To do this, we
 would have to ensure that every array had its own block (or check in the
 write barrier, which adds even more expense).

-- 
Ticket URL: <http://cvs.haskell.org/trac/ghc/ticket/650>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler


More information about the Glasgow-haskell-bugs mailing list