31

Haskell doesn't feature explicit memory management, and all objects are passed by value, so there's no obvious reference counting or garbage collection either. How does a Haskell compiler typically decide whether to generate code that allocates on the stack versus code that allocates on the heap for a given variable? Will it consistently heap or stack allocate the same variables across different call sites for the same function? And when it allocates, how does it decide when to free memory? Are stack allocations and deallocations still performed in the same function entrance/exit pattern as in C?

trincot
  • 317,000
  • 35
  • 244
  • 286
Joseph Garvin
  • 20,727
  • 18
  • 94
  • 165
  • As all values are immutable anyway, you wouldn't notice if the Haskell compiler optimized copying away and instead used pointers to the same value in several places. In fact, at least GHC does this optimization. –  Feb 27 '11 at 09:50
  • 15
    It is not quite correct to say that "all objects are passed by value" in Haskell. In Haskell, referential transparency is enforced -- which means that passing by value and passing by reference yield identical results in all cases. This allows the compiler to decide on a case-by-case basis whether to pass a particular parameter by reference or by value. The same is true of structure members. – Dietrich Epp Feb 27 '11 at 10:02
  • 1
    I figured this was probably the case, that's what lead me to ask whether it's guaranteed to be the same across call sites. A better way of putting it would have been: since the compiler seemingly has freedom to choose when to stack allocate and when to heap allocate, will a given function always have the same calling convention? nominolo's answer makes me think no. – Joseph Garvin Feb 27 '11 at 16:24

2 Answers2

38

When you call a function like this

f 42 (g x y)

then the runtime behaviour is something like the following:

p1 = malloc(2 * sizeof(Word))
p1[0] = &Tag_for_Int
p1[1] = 42
p2 = malloc(3 * sizeof(Word))
p2[0] = &Code_for_g_x_y
p2[1] = x
p2[2] = y
f(p1, p2)

That is, arguments are usually passed as pointers to objects on the heap like in Java, but unlike Java these objects may represent suspended computations, a.k.a. thunks, such as (g x y/p2) in our example. Without optimisations, this execution model is quite inefficient, but there are ways to avoid many of these overheads.

  1. GHC does a lot of inlining and unboxing. Inlining removes the function call overhead and often enables further optimisations. Unboxing means changing the calling convention, in the example above we could pass 42 directly instead of creating the heap object p1.

  2. Strictness analysis finds out whether an argument is guaranteed to be evaluated. In that case, we don't need to create a thunk, but evaluate the expression fully and then pass the final result as an argument.

  3. Small objects (currently only 8bit Chars and Ints) are cached. That is, instead of allocating a new pointer for each object, a pointer to the cached object is returned. Even though the object is initially allocated on the heap, the garbage collector will de-duplicate them later (only small Ints and Chars). Since objects are immutable this is safe.

  4. Limited escape analysis. For local functions some arguments may be passed on the stack, because they are known to be dead code by the time the outer function returns.

Edit: For (much) more information see "Implementing Lazy Functional Languages on Stock Hardware: The Spineless Tagless G-machine". This paper uses "push/enter" as the calling convention. Newer versions of GHC use the "eval/apply" calling convention. For a discussion of the trade-offs and reasons for that switch see "How to make a fast curry: push/enter vs eval/apply"

nominolo
  • 5,085
  • 2
  • 25
  • 31
  • 1
    Since you mentioned escape analysis: would it be possible to optimize `x ++ y` to avoid copying of `x` (just change the last tail pointer to point to `y`), if `x` is not used anywhere else? Does GHC do this? – Roman Cheplyaka Feb 27 '11 at 13:47
  • @Roman: No, GHC's escape analysis only applies to local `let`-bindings. There is no interprocedural analysis going on. For your example, you'd need to statically ensure that there is no other pointer anywhere in the heap that points to `x` or any of its successors. You'd need linear or uniqueness types to prove something like that. – nominolo Feb 27 '11 at 14:00
  • 2
    Due to the optimizations, does this mean every function may have multiple calling conventions? I realize for example that if the function is inlined, it doesn't really have a calling convention anymore and it might get changed arbitrarily in the function that it's inlined into, but if say the function is called from another compilation unit (which therefore won't have access to the definition and can't inline the function), will the function have a consistent calling convention? Or does GHC generate multiple versions of each function to cover its bases or ... ? – Joseph Garvin Feb 27 '11 at 16:30
  • @Joseph: A single function can only have a single calling convention. However, the compiler may generate additional functions. A common technique is the Worker/Wrapper transformation, where a recursive function is transformed into non-recursive wrapper and a recursive worker which uses a very efficient calling convention. If the wrapper is inlined at the call site then the code will automatically call the more efficient worker (which uses the faster calling convention.) This is done if the function is (1) recursive, and (2) provably strict in a particular argument. – nominolo Feb 27 '11 at 18:04
  • 1
    Another great reference is the ghc wiki: http://hackage.haskell.org/trac/ghc/wiki/Commentary/Rts/HaskellExecution/FunctionCalls – sclv Feb 28 '11 at 01:09
  • +1 for the link to STG machine papers - they are a very enlightening (and fascinating) read. – mokus Mar 02 '11 at 14:10
  • @nominolo I think it might actually be a good idea for GHC to use or emuluate uniqueness types internally (not necessarily to add them to the language) for figuring out when it's safe to modify data in place. – Jeremy List Jan 01 '15 at 15:19
2

The only things GHC puts on the stack are evaluation contexts. Anything allocated with a let/where binding, and all data constructors and functions, are stored in the heap. Lazy evaluation makes everything you know about execution strategies in strict languages irrelevant.

Carl
  • 26,500
  • 4
  • 65
  • 86
  • are you sure ? As mentionned by nominolo, with the help of Escape Analysis, some objects could be allocated on the stack if we know they won't be needed once the call has completed. – Matthieu M. Feb 27 '11 at 11:17
  • +1 It's not a rock solid answer, but it's sorta true. Haskell is about crafting programs with a functional/declarative mindset; you're (sort of) "not supposed to" be worried about how the compiler actually makes it happen. – Dan Burton Feb 27 '11 at 22:40
  • 2
    What's an evaluation context? – MasterMastic Feb 12 '15 at 03:53
  • 1
    @DanBurton: someone has to implement the compiler ;) – Joseph Garvin Jul 13 '15 at 16:37