System.Mem.performGC does a major GC. When would a partial GC be enough?
I've described the image-processing example a bunch of times.
We have an external resource (e.g., memory used to store images) which is somewhat abundant and cheap but not completely free (e.g., eventually you start to swap). It is used up at a different rate than the Haskell heap so Haskell GCs don't occur at the right times to keep the cost low and we want to trigger GCs ourselves.
Hmmm, the garbage collector is a black box and has its own complicated heuristics for managing memory usage, but you are describing a mechanism that depends rather heavily on certain assumed behaviours. At the least, that gives the garbage collector less flexibility to change its own behaviour, lest it invalidate the assumptions made by the external allocator.
(In the image processing example, images were megabytes and an expression like (x + (y * mask)) would generate 2 intermediate images (several megabytes) while doing just 2 reductions in Haskell.)
I think I'd be tempted to try to use a more predictable allocation scheme given the size of the objects involved. Perhaps arenas?
How often and how hard should we GC? We can't do a full GC too often, or we'll spend a lot of time GCing, destroy our cache and cause premature promotion of Haskell objects into the old generation which will make the GC behave poorly. So if all we can do is a full GC, we'll GC rarely and use a lot of the external resource.
Suppose we could collect just the allocation arena. That would be much less expensive (time taken, effect on caches, confusion of object ages) but not always effective. It would start out cheap and effective but more and more objects would slip into older generations and have to wait for a full GC.
To achieve any desired tradeoff between GC cost and excess resource usage, we want a number of levels of GC: gc1, gc2, gc3, gc4, ... Each one more effective than the last and each one more expensive than the last. We'll use gc1 most often, gc2 less often, gc3 occasionally, gc4 rarely, ...
But there seems to be no way to reasonably decide how often one should call these. Doesn't it depend on the garbage collector's own parameters too?
I think the spec should be clarified along these lines:
Header files have no impact on the semantics of a foreign call, and whether an implementation uses the header file or not is implementation-defined. Some implementations may require a header file which supplies a correct prototype for the function in order to generate correct code.
I still don't like the fact that compilers are free to ignore header files. Labelling it an error instead of a change in semantics doesn't affect the fact that portability is compromised.
I don't see any alternative - would you require a compiler that has only a native code generator to read header files? When there's no C compiler on the system? (this is realistic - at some point we'd like to make the via-C route in GHC completely optional, so we can ship a compiler on Windows that doesn't need to be bundled with GCC).
Perhaps on GHC you should be required to "register" the top module in your program first, maybe something like
registerModule(__stginit_Main);
that way you can register multiple modules (which isn't possible at the moment, you have to have another module which imports all the others).
What does that do? Is it for threading, GC, profiling, ...?
Each module has a little initialisation fragment that calls all the initialisation fragments for the modules it imports. At the moment, there are two kinds of initialisation done for each module: - each foreign export is registered as a stable pointer. This prevents the garbage collector from collecting any CAFs which might be required (indirectly) by a foreign export. - when profiling, all the cost centres in the current module are initialised. It might be possible to do this using linker sets, but I haven't tried (and it would probably be highly non-portable too). Cheers, Simon
Hmmm, the garbage collector is a black box and has its own complicated heuristics for managing memory usage, but you are describing a mechanism that depends rather heavily on certain assumed behaviours. At the least, that gives the garbage collector less flexibility to change its own behaviour, lest it invalidate the assumptions made by the external allocator.
This is a situation where something is better than nothing. The heuristics we used with Hugs were pretty lame -I don't think they even worked as I'd intended- but they were good enough to get the job done. In fact, the application ran brilliantly: small GC pauses, little wasted memory. So, no, I don't see a heavy dependence or an extreme sensitivity to GC behaviour.
(In the image processing example, images were megabytes and an expression like (x + (y * mask)) would generate 2 intermediate images (several megabytes) while doing just 2 reductions in Haskell.)
I think I'd be tempted to try to use a more predictable allocation scheme given the size of the objects involved. Perhaps arenas?
Aren't arenas a form of explicit memory management? Explicit memory management was inappropriate for this example because the objects were manipulated by non-IO operations: programmers couldn't track the lifetime of an object because it depended on the order that thunks were evaluated. Note that this is _exactly_ what MallocPtrs/ ForeignObjs/ ForeignPtrs were invented to deal with. (Indeed, the first ever application was a (different) image processing library.)
But there seems to be no way to reasonably decide how often one should call these. Doesn't it depend on the garbage collector's own parameters too?
It probably does. It might even vary from one kind of application to another. But I don't think it is very sensitive to those parameters. The goal here is to behave tolerably (e.g., not do a full GC on a 100Mb heap 20 times per second) not optimally. [other topics answered separately] -- Alastair
Alastair Reid <alastair@reid-consulting-uk.ltd.uk> wrote,
Hmmm, the garbage collector is a black box and has its own complicated heuristics for managing memory usage, but you are describing a mechanism that depends rather heavily on certain assumed behaviours. At the least, that gives the garbage collector less flexibility to change its own behaviour, lest it invalidate the assumptions made by the external allocator.
This is a situation where something is better than nothing. The heuristics we used with Hugs were pretty lame -I don't think they even worked as I'd intended- but they were good enough to get the job done. In fact, the application ran brilliantly: small GC pauses, little wasted memory.
I am not sure what to do with this one. SimonM, did Alastair convince you by now that what he proposes is useful? I understand the problem, but I am also not really sure whether the proposed solution would work portably. If performGC gets a kind of "forcefulness" level in the form of natural number (0 ~ GC if you like to, ..., 9 ~ do a major GC and get rid of everything you can), implementations would be free to more or less ignore the parameter (might have a simple two-space collector) or try to provide several levels of GC (in whatever way fits into the GC policies of the implementation). Nevertheless, it should still help Alastair when using a system that honours use of the forcefulness level. Manuel
I think the thing to do is add the existing performGC to the standard (perhaps giving it an hs_ prefix in the process) and leave development of an extended version of the function for when the GHC folk (or anyone else with a generational collector) decide they want a forcefulness argument. Come that day, we'd define: void performGC(void) { performPartialGC(0); } (or whatever it is you do to force a full collection). A
Alastair Reid <alastair@reid-consulting-uk.ltd.uk> wrote,
I think the thing to do is add the existing performGC to the standard (perhaps giving it an hs_ prefix in the process) and leave development of an extended version of the function for when the GHC folk (or anyone else with a generational collector) decide they want a forcefulness argument. Come that day, we'd define:
void performGC(void) { performPartialGC(0); }
(or whatever it is you do to force a full collection).
Ok. So, how about void hs_perform_gc (void); with the following specification: The function hs_perform_gc() advices the Haskell storage manager to perform a garbage collection. This function must not be invoked from C functions that are imported "unsafe" into Haskell code. Do you want a stronger wording on what kind of garbage collection is to be performed or do we want to keep it deliberately unspecified (ie, leave it to the individual Haskell system)? Cheers, Manuel
Do you want a stronger wording on what kind of garbage collection is to be performed or do we want to keep it deliberately unspecified (ie, leave it to the individual Haskell system)?
It'd be nice to say that it has to be a full GC - but I've no idea how to specify that in a non-operational (i.e., implementation dependent) way. We could insert the word 'full' and leave it to people's imaginations? -- Alastair Reid alastair@reid-consulting-uk.ltd.uk Reid Consulting (UK) Limited http://www.reid-consulting-uk.ltd.uk/alastair/
At the moment, there are two kinds of initialisation done for each module:
Both ELF and DLLs on Windows provide a way of specifying initializers. Or, easier yet, since the user is already using the hs_init function, you could use that. The way you'd do that in ELF is to define a special section 'hs_initializers'. Every module would contain a single object in that section: the address of the initializer. (In gcc you do this by attaching an attribute to the variable. In asm it is even easier since assemblers directly expose sections to the programmer.) The linker will do what it does with all sections: concatenate the pieces from all object files. hs_init would treat the section as an array of function pointers. I'm sure that the Windows linker must have a similar mechanism - the main trick is figuring out what name to give the sections. -- Alastair
participants (3)
-
Alastair Reid -
Manuel M T Chakravarty -
Simon Marlow