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

GHC trac at galois.com
Thu Jan 12 09:52:28 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            |     Resolution:         
  Keywords:                    |             Os:  Unknown
Difficulty:  Moderate (1 day)  |   Architecture:  Unknown
-------------------------------+--------------------------------------------
Old description:

> 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).

New description:

 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).

 == IORefs ==

 This also affects IORefs, which are essentially single-element IOArrays.
 I just noticed that GHC often has a large number of IORefs hanging around
 in the heap from the typechecker, and the cost of traversing the mutable
 list can dominate minor GCs.

-- 
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