Distinct closure types vs. known infotables for stack frames

Hello all, I am tinkering with the RTS again while trying to fix #23513 https://gitlab.haskell.org/ghc/ghc/-/issues/23513, and every time I touch the exceptions/continuations code, I find myself waffling about whether to introduce more closure types. I’d like to get a second opinion so I can stop changing my mind! Currently, we have distinct closure types for certain special stack frames, like CATCH_FRAME, ATOMICALLY_FRAME, and UNDERFLOW_FRAME. However, there are other special stack frames that *don’t* get their own closure types: there is no MASK_ASYNC_EXCEPTIONS_FRAME or PROMPT_FRAME. Instead, when code needs to recognize these frames on the stack, it just looks for a known infotable pointer. That is, instead of writing if (frame->header.info->i.type == PROMPT_FRAME) { ... } we write if (*frame == &stg_prompt_frame_info) { ... } which works out because there’s only one info table that’s used for all prompt frames. There are a handful of stack frames that are recognized in this way by some part of the RTS, but the criteria used to determine which frames get their own types and which don’t is not particularly clear. For some frames, like UPDATE_FRAME, the closure type is necessary because it is shared between several infotables. But other types, like CATCH_FRAME and UNDERFLOW_FRAME, are only ever used by precisely one infotable. I think one can make the following arguments for/against using separate closure types for these stack frames: - Pro: It’s helpful to have separate types for particularly special frames like UNDERFLOW_FRAME because it makes it easier to remember which special cases to handle when walking the stack. - Pro: Branching on stack frame closure types using switch is easier to read than comparing infotable pointers. - Con: Adding more closure types unnecessarily pollutes code that branches on closure types, like the garbage collector. - Con: Using special closure types for these frames might make it seem like they have some special layout when in fact they are just ordinary stack frames. Does anyone have any opinions about this? I’m personally okay with the status quo, but the inconsistency makes me constantly second-guess whether someone else might feel strongly that I ought to be doing things the other way! Thanks, Alexis

Thanks for a clear writeup, Alexis. My instinct is to do it all with closure types, not pointer comparison. -
Con: Adding more closure types unnecessarily pollutes code that branches on closure types, like the garbage collector.
-
Con: Adding more closure types unnecessarily pollutes code that branches on closure types, like the garbage collector.
- I don't get this. Different stack frames might have different layouts
(e.g a catch frame has a fixed size with with exactly two (or whatever)
pointers, etc). This isn't pollution. Just as every heap closure has a
closure type that describes its layout, so does every stack frame.
Perhaps you mean that in fact all stack frames share a single layout, with
a bitmap to describe it? That seems sub-optimal for these special frames
where we statically know the entire shape!
Perhaps you mean that many stack frames share a common layout, so that the
case analysis on closure type might have many cases all pointing to the
same GC code. But if so, isn't that true for heap closures too? If we are
concerned about that, we could have two "type" fields in the info table,
one exclusively concerned with layout, so that the GC could just branch on
that and only have a few cases to consider, and one with a finer
granularity.
In short, why are the design considerations for stack frames different to
heap objects? I think of a stack frame simply as a heap object that
happens to be allocated on the stack
Simon
On Mon, 26 Jun 2023 at 21:21, Alexis King
Hello all,
I am tinkering with the RTS again while trying to fix #23513 https://gitlab.haskell.org/ghc/ghc/-/issues/23513, and every time I touch the exceptions/continuations code, I find myself waffling about whether to introduce more closure types. I’d like to get a second opinion so I can stop changing my mind!
Currently, we have distinct closure types for certain special stack frames, like CATCH_FRAME, ATOMICALLY_FRAME, and UNDERFLOW_FRAME. However, there are other special stack frames that *don’t* get their own closure types: there is no MASK_ASYNC_EXCEPTIONS_FRAME or PROMPT_FRAME. Instead, when code needs to recognize these frames on the stack, it just looks for a known infotable pointer. That is, instead of writing
if (frame->header.info->i.type == PROMPT_FRAME) { ... }
we write
if (*frame == &stg_prompt_frame_info) { ... }
which works out because there’s only one info table that’s used for all prompt frames.
There are a handful of stack frames that are recognized in this way by some part of the RTS, but the criteria used to determine which frames get their own types and which don’t is not particularly clear. For some frames, like UPDATE_FRAME, the closure type is necessary because it is shared between several infotables. But other types, like CATCH_FRAME and UNDERFLOW_FRAME, are only ever used by precisely one infotable.
I think one can make the following arguments for/against using separate closure types for these stack frames:
-
Pro: It’s helpful to have separate types for particularly special frames like UNDERFLOW_FRAME because it makes it easier to remember which special cases to handle when walking the stack. -
Pro: Branching on stack frame closure types using switch is easier to read than comparing infotable pointers. -
Con: Adding more closure types unnecessarily pollutes code that branches on closure types, like the garbage collector. -
Con: Using special closure types for these frames might make it seem like they have some special layout when in fact they are just ordinary stack frames.
Does anyone have any opinions about this? I’m personally okay with the status quo, but the inconsistency makes me constantly second-guess whether someone else might feel strongly that I ought to be doing things the other way!
Thanks, Alexis _______________________________________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs

On Tue, Jun 27, 2023 at 4:13 AM Simon Peyton Jones < simon.peytonjones@gmail.com> wrote:
In short, why are the design considerations for stack frames different to heap objects? I think of a stack frame simply as a heap object that happens to be allocated on the stack
I agree with this perspective—I think it is generally an accurate one. Indeed, I think it may very well be true that what I’ve described largely applies to heap objects as well as stack frames, and working on continuations just means I’ve much more time thinking about stacks. Perhaps if I were working on the garbage collector I would be asking the same question about heap objects. For example, we have MVAR_CLEAN and MVAR_DIRTY, but each of those types is only used by one statically-allocated infotable, as far as I can tell. In some parts of the code, we check that the closure type is MVAR_CLEAN or MVAR_DIRTY, but in other places, we check whether the infotable is stg_MVAR_CLEAN_info or stg_MVAR_DIRTY_info. Meanwhile, we have both stg_TVAR_CLEAN_info and stg_TVAR_DIRTY_info, but they share the same TVAR closure type! The decisions here seem fairly arbitrary. But perhaps there is some method to the madness, or perhaps someone prefers one approach over the others, in which case I would like to hear it! And if not, well, at least I’ll know. :)

The decisions here seem fairly arbitrary. But perhaps there is some method to the madness, or perhaps someone prefers one approach over the others, in which case I would like to hear it! And if not, well, at least I’ll know. :)
I suspect it's all just happenstance. Maybe @Ben Gamari
On Tue, Jun 27, 2023 at 4:13 AM Simon Peyton Jones < simon.peytonjones@gmail.com> wrote:
In short, why are the design considerations for stack frames different to heap objects? I think of a stack frame simply as a heap object that happens to be allocated on the stack
I agree with this perspective—I think it is generally an accurate one. Indeed, I think it may very well be true that what I’ve described largely applies to heap objects as well as stack frames, and working on continuations just means I’ve much more time thinking about stacks. Perhaps if I were working on the garbage collector I would be asking the same question about heap objects.
For example, we have MVAR_CLEAN and MVAR_DIRTY, but each of those types is only used by one statically-allocated infotable, as far as I can tell. In some parts of the code, we check that the closure type is MVAR_CLEAN or MVAR_DIRTY, but in other places, we check whether the infotable is stg_MVAR_CLEAN_info or stg_MVAR_DIRTY_info. Meanwhile, we have both stg_TVAR_CLEAN_info and stg_TVAR_DIRTY_info, but they share the same TVAR closure type!
The decisions here seem fairly arbitrary. But perhaps there is some method to the madness, or perhaps someone prefers one approach over the others, in which case I would like to hear it! And if not, well, at least I’ll know. :)

Alexis King
Hello all,
I am tinkering with the RTS again while trying to fix #23513 https://gitlab.haskell.org/ghc/ghc/-/issues/23513, and every time I touch the exceptions/continuations code, I find myself waffling about whether to introduce more closure types. I’d like to get a second opinion so I can stop changing my mind!
Indeed I have had similar questions in the past. In principle, I personally think that using closure types to distinguish special info tables is conceptually cleaner. However, the trouble is that it is also, at least in principle, more costly. Specifically, branching on closure type requires that we examine the info table, which may incur a cache/TLB miss. While one would hope and expect that "common" info tables are already in a nearby cache, it's ultimately very easy to side-step this cost entirely by simply branching on the info table pointer. This tension has lead me to waffle in similar ways as you report. On the other hand, it seems hard to avoid given the tricky trade-offs involved. My (rather loose) policy when working on the RTS has typically been: * When introducing a new closure/stack-frame info table that differs markedly from any that already exist, give it a new closure type. * When matching on the info table, follow the example of surrounding code when deciding whether to match on the closure type or info table pointer * If there is no appropriate nearby similar logic to follow, use my gut to assess how hot the check will be and unless it is quite warm, use the closure type. However, I'll admit that this policy likely only contributes to the inconsistency. Perhaps you have some clever idea on how things could be improved? Cheers, - Ben
participants (3)
-
Alexis King
-
Ben Gamari
-
Simon Peyton Jones