How unsafe are unsafeThawArray# and unsafeFreezeArray#

Hi, I just ran across some code that calls unsafeThawArray#, writeArray#, and unsafeFreezeArray#, in that order. How unsafe is that? * Is it unsafe in the sense that if someone has a reference to the original Array# they will see the value of that pure array change? * Is it unsafe in the sense things will crash and burn? I looked at the implementation of unsafeFreezeArray#, which is emitPrimOp [res] UnsafeFreezeArrayOp [arg] _ = stmtsC [ setInfo arg (CmmLit (CmmLabel mkMAP_FROZEN_infoLabel)), CmmAssign (CmmLocal res) arg ] but I couldn't find any implementation of unsafeThawArray#. Is it a no-op? It seems to me that if unsafeFreezeArray# changes the head of the array heap object so should unsafeThawArray#. Cheers, Johan

On 09/03/2012 01:13, Johan Tibell wrote:
Hi,
I just ran across some code that calls unsafeThawArray#, writeArray#, and unsafeFreezeArray#, in that order. How unsafe is that?
* Is it unsafe in the sense that if someone has a reference to the original Array# they will see the value of that pure array change?
Yes.
* Is it unsafe in the sense things will crash and burn?
No. (at least, we would consider it a bug if a crash was the result)
I looked at the implementation of unsafeFreezeArray#, which is
emitPrimOp [res] UnsafeFreezeArrayOp [arg] _ = stmtsC [ setInfo arg (CmmLit (CmmLabel mkMAP_FROZEN_infoLabel)), CmmAssign (CmmLocal res) arg ]
but I couldn't find any implementation of unsafeThawArray#. Is it a no-op? It seems to me that if unsafeFreezeArray# changes the head of the array heap object so should unsafeThawArray#.
You'll find the implementation of unsafeThawArray# in rts/PrimOps.cmm, reproduced here for your enjoyment: stg_unsafeThawArrayzh { // SUBTLETY TO DO WITH THE OLD GEN MUTABLE LIST // // A MUT_ARR_PTRS lives on the mutable list, but a MUT_ARR_PTRS_FROZEN // normally doesn't. However, when we freeze a MUT_ARR_PTRS, we leave // it on the mutable list for the GC to remove (removing something from // the mutable list is not easy). // // So that we can tell whether a MUT_ARR_PTRS_FROZEN is on the mutable list, // when we freeze it we set the info ptr to be MUT_ARR_PTRS_FROZEN0 // to indicate that it is still on the mutable list. // // So, when we thaw a MUT_ARR_PTRS_FROZEN, we must cope with two cases: // either it is on a mut_list, or it isn't. We adopt the convention that // the closure type is MUT_ARR_PTRS_FROZEN0 if it is on the mutable list, // and MUT_ARR_PTRS_FROZEN otherwise. In fact it wouldn't matter if // we put it on the mutable list more than once, but it would get scavenged // multiple times during GC, which would be unnecessarily slow. // if (StgHeader_info(R1) != stg_MUT_ARR_PTRS_FROZEN0_info) { SET_INFO(R1,stg_MUT_ARR_PTRS_DIRTY_info); recordMutable(R1, R1); // must be done after SET_INFO, because it ASSERTs closure_MUTABLE() RET_P(R1); } else { SET_INFO(R1,stg_MUT_ARR_PTRS_DIRTY_info); RET_P(R1); } } Cheers, Simon

Out of curiosity, Is the reason you keep track of mutable vs not mutable heap allocations in order to optimize the generational garbage collector? as in, if a non-mutable value is placed in an older generation you don't need to worry about it being updated with a link to a newer one or is there another reason you keep track of it? Is it a pure optimization or needed for correctness? A weakness of jhc right now is its stop the world garbage collector, so far, I have been mitigating it by not creating garbage whenever possible, I do an escape analysis and allocate values on the stack when possible, and recognize linear uses of heap value in some circumstances and re-use heap locations directly (like when a cons cell is deconstructed and another is constructed right away I can reuse the spacue in certain cases) but eventually a full GC needs to run and it blocks the whole runtime which is particularly not good for embedded targets (where jhc seems to be thriving at the moment.). My unsafeFreeze and unsafeThaw are currently NOPs. frozen arrays are just a newtype of non-frozen ones (see http://repetae.net/dw/darcsweb.cgi?r=jhc;a=headblob;f=/lib/jhc-prim/Jhc/Prim...) John

On 09/03/2012 11:44, John Meacham wrote:
Out of curiosity, Is the reason you keep track of mutable vs not mutable heap allocations in order to optimize the generational garbage collector? as in, if a non-mutable value is placed in an older generation you don't need to worry about it being updated with a link to a newer one or is there another reason you keep track of it? Is it a pure optimization or needed for correctness?
It's for correctness - this is how we track all the pointers from the old generation to the new generation. There are lots of different ways of doing generational GC write barriers, and indeeed GHC uses a mixture of different methods: a remembered set for updated thunks and mutated IORefs, and a card table for arrays. Mutable arrays normally stay in the remembered set continuously, so that the write barrier for array mutation doesn't have to worry about adding the array to the remembered set, but it does modify the card table. An immutable array can be dropped from the remembered set, but only once all its pointers are safely pointing to the old generation. But then, unsafeThawArray has to put it back in the remembered set again. Once upon a time all the mutable objects were in the remembered set, but gradually we've been moving away from this and adding them to the remembered set only when they get modified. There are a few objects that still use the old method: TVars, for example.
A weakness of jhc right now is its stop the world garbage collector, so far, I have been mitigating it by not creating garbage whenever possible, I do an escape analysis and allocate values on the stack when possible, and recognize linear uses of heap value in some circumstances and re-use heap locations directly (like when a cons cell is deconstructed and another is constructed right away I can reuse the spacue in certain cases) but eventually a full GC needs to run and it blocks the whole runtime which is particularly not good for embedded targets (where jhc seems to be thriving at the moment.). My unsafeFreeze and unsafeThaw are currently NOPs. frozen arrays are just a newtype of non-frozen ones (see http://repetae.net/dw/darcsweb.cgi?r=jhc;a=headblob;f=/lib/jhc-prim/Jhc/Prim...)
Interesting - we do generational GC instead of escape analysis and other "compile-time GC" techniques. IMO, generational GC is a necessity, and once you have it, you don't need to worry so much about short-lived allocation because it all gets recycled in the cache. (try +RTS -G1 sometime to see the difference between generational GC and ordinary 2-space copying). On the other hand, generational GC does give rise to some strange performance characteristics, especially with mutable objects and sometimes laziness. And it doesn't solve the pause time problem, except that the long pauses are less frequent. Improving pause times is high up my list of things we want to do in the GC. Cheers, Simon

On March 9, 2012 06:44:33 John Meacham wrote:
A weakness of jhc right now is its stop the world garbage collector, so far, I have been mitigating it by not creating garbage whenever possible, I do an escape analysis and allocate values on the stack when possible, and recognize linear uses of heap value in some circumstances and re-use heap locations directly (like when a cons cell is deconstructed and another is constructed right away I can reuse the spacue in certain cases) but eventually a full GC needs to run and it blocks the whole runtime which is particularly not good for embedded targets (where jhc seems to be thriving at the moment.). My unsafeFreeze and unsafeThaw are currently NOPs. frozen arrays are just a newtype of non-frozen ones (see http://repetae.net/dw/darcsweb.cgi?r=jhc;a=headblob;f=/lib/jhc-prim/Jhc/Pri m/Array.hs)
Here's an idea I've had bouncing around in the back of my head for a couple of years. I haven't found the time to think it out in full detail yet, so I appoligize if it has some obvious flaw or has already been done. The idea is to add a primative/runtime function gc :: a -> a that implements a garbage collection scheme. That is, say gc expression occurs in the the code somewhere. At some point expression may need to be evaluated to WHNF. When this occurs, the effect of 'gc' is that all the memory allocations required to do this come out of a new pool of memory. When the reduction is complete, the final value is treated as the root in this region and and it and all reachable points in the region are copied out. The region is then released as a whole and evaluation continues. The idea is this allows the programmer to control where garbage collection occurs (it only occurs on reduction of 'gc expression' expressions) and put bounds on how long it will take (it will only take as much time as it takes to walk the structure of the returned value -- a function of that value). I believe it is composable and can gives you what you need to reason about garbage collection bounds, the only real catch I see is that lazyness could make this reasoning hard due to determining exactly were reduction occurs. Cheers! -Tyson

On 2012-03-09 02:13, Johan Tibell wrote:
Hi,
I just ran across some code that calls unsafeThawArray#, writeArray#, and unsafeFreezeArray#, in that order. How unsafe is that?
Quick follow up question: Are unsafeThawArray# and unsafeFreezeArray# guaranteed to not make a copy? I.e. are these two pieces of code equivalent with respect to further usage of the return value: do mary <- unsafeThawArray# ary writeArray# mary 123# newValue unsafeFreezeArray# mary and do mary <- unsafeThawArray# ary writeArray# mary 123# newValue return ary (where do notation is actually done manually.) Twan

On Fri, Mar 9, 2012 at 11:39 AM, Twan van Laarhoven
Quick follow up question:
Are unsafeThawArray# and unsafeFreezeArray# guaranteed to not make a copy? I.e. are these two pieces of code equivalent with respect to further usage of the return value:
do mary <- unsafeThawArray# ary writeArray# mary 123# newValue unsafeFreezeArray# mary
and
do mary <- unsafeThawArray# ary writeArray# mary 123# newValue return ary
(where do notation is actually done manually.)
I believe you * don't have to use the return value from unsafeFreezeArray#, you can use the (now mutated) Array# initially passed to unsafeThawArray#, but * you must call unsafeFreezeArray# to make sure the header of the heap object is updated correctly. At least this is what I did in unordered-containers here: https://github.com/tibbe/unordered-containers/commit/428885cb1a62123921c22bc... Cheers, Johan
participants (5)
-
Johan Tibell
-
John Meacham
-
Simon Marlow
-
Twan van Laarhoven
-
Tyson Whitehead