Plans to bring FBIP/FP2 in Haskell?

It seems that in the recent years, there have been advancements towards the in-place implementations for functional programs, as seen in Lean 4 and Roc and Koka, and several academic papers.

Are there any plans to implement such strategies in Haskell (i.e, GHC)? I did a quick Google search but couldn’t find any discussion.


  • Lorenzen, Anton, Daan Leijen, and Wouter Swierstra. “FP²: Fully in-Place Functional Programming.” ICFP (2023)
  • Ullrich, Sebastian, and Leonardo de Moura. “Counting immutable beans: Reference counting optimized for purely functional programming.”
  • Reinking, Alex, et al. “Perceus: Garbage free reference counting with reuse.” PLDI (2021)
7 Likes

Dynamic reuse relies on reference counting as garbage collection strategy. Thunks and mutable data structures introduce cycles in the heap, so you cannot use reference counting (exclusively) to collect such a heap. Therefore I don’t see how to apply these techniques to GHC Haskell.

On the other hand, transient data structures offer the same promise of in-place update. It’s just that GHC’s support for mutable data structures (e.g. record fields) is lacking, so there is an inherent overhead to transients.

I mentored a bachelor’s thesis that used a transient implementation for GHC’s InScopeSet: https://pp.ipd.kit.edu/uploads/publikationen/ellinger22bachelorarbeit.pdf

The results on p31 were mixed, but I think they’d be a win overall if more code was actually written in a way that exploited mutability more (which would’ve required large refactorings).

9 Likes

Another thing that would be worth exploring is to implement something like this through staged fusion (ie, (typed) Template Haskell). Like GitHub - AndrasKovacs/staged-fusion: Staged push/pull fusion with typed Template Haskell or GitHub - phadej/staged: Staged Streams and other stuff

1 Like

in order to build and modify heap closures in place, you can exploit the compact region feature. this is deeply cursed territory and has many footguns, but my colleague @tbagrel1 did some explorations on this front and he might be motivated to provide a bit of his experience here :slight_smile:

5 Likes

Thanks @TerrorJack for mentionning me.

I indeed used Compact Regions as a way to obtain immovable memory area in which the GC won’t go looking. That way, I could do controlled mutations without fear of letting the RTS read uninitialized pointers etc. The end goal was to enable destination-passing style programming in Haskell, but I guess that the same tricks could be used to implement FiP programming too.

Here’s the major part of my work:

AFAIK, doing the same kind of mutations in the garbage-collected heap would require a very precise understanding of garbage collection/RTS in GHC, and alas, I don’t have it. I’m also afraid it would be even more brittle than what I propose in my work, as any modification of the GC implem afterwards would require to review and adapt the mutation system again.

Personnally, I used compact regions almost solely to avoid the GC. But maybe there are alternate memory allocation systems that could be even better for that purpose.

5 Likes

I don’t know the GHC compiler or runtime well at all, but would it not be possible for compile time reuse analysis to produce code that replaces values in single-use constructors of same size?

For example, could a function f :: Int → Int be compiled to not allocate a new I# and instead replace the value in the supplied I# with the result, as long as the caller is the producer of that input and only uses it when passing it to f?

Of course, the compiler might have to produce two versions of f, one of them for cases where this cannot be applied.

So I don’t like making promises on something that isn’t public yet, but this is what I have been doing for the last 2 years during my free time. I have been building a new Haskell compiler entirely from scratch and I plan to use FBIP. I plan on releasing it and make a proper announcement when I get to the MVP (if I get there tbh).

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Haskell                        177           1302            364          10998
-------------------------------------------------------------------------------
SUM:                           177           1302            364          10998
-------------------------------------------------------------------------------
8 Likes

In order to allow reuse of a heap object, you need to know whether the object is dead at the point where you want to reuse it. So we are talking about a static analysis that estimates the lifetime of heap objects. (How reuse is implemented is a different matter, but as you note it is doable.)

I don’t have much experience with such analyses and am having doubts about its precision (i.e., that it justifies much reuse in the end). But one thesis I mentored on this topic is an escape analysis. That is, an analysis that infers whether some heap object does not live longer than its allocating function and hence can be allocated on the stack: https://pp.ipd.kit.edu/uploads/publikationen/scheper22masterarbeit.pdf
The results here need to be taken with a grain of salt: Although the portion of heap objects that can be allocated on the stack is huge, certainly some of the ensuing stack allocation is not safe for space, that is, the stack allocated objects will occupy stack space longer than they are actually alive, which may lead to an asymptotic increase in total memory usage.

However, maybe an escape analysis like this could be useful to estimate whether reuse of some heap object is possible. I’d also hope that there are more advanced attempts to reuse analysis. However, a static reuse analysis will yield far smaller benefits than a dynamic one based on reference counting.

This is cool :slight_smile: Presumably, your implementation will not memoise thunks, at the very least for recursive thunks? Or at least I can hardly imagine that

ones = 1 : ones

will be represented as a “loopy list” at runtime, that is, a circular data structure.

Indeed for the non-recursive thunk case, FBIP seems possible (as long as recursive thunks are ruled out).

1 Like

I also know of this thesis, but I wasn’t involved:

Correct.

Though it’s okay to have global recursive thunks because they don’t have a reference count. Right now my plan is to use lambda lifting to rewrite all recursive let bindings into global let bindings.

1 Like

How do you track the lifetime of global thunks if they don’t have a reference count? GHC calls these “constant applicative form” and is careful to release them once they are dead. IIRC it’s kind of a big deal (though I wasn’t around when this hadn’t been implemented yet).

You don’t. They’re global variables, they’re supposed to leak. I don’t want to have a runtime that needs to know what main is doing or what threads are currently running. Being able to write shared library that you can use from C is one of my goals.

I’m fully aware that will break the big O characteristics of certain Haskell programs but I’m okay with that. Haskell 2010 is dead anyway, so almost all packages will need to be ported to avoid GHC features I don’t implement either way.

2 Likes