mulle-objc MetaABI — Dispatch Decision Chart

· nat's blog


Three independent mechanisms classify every method call identically:

Path 1 — Compiler    : typeNeedsMetaABIAlloca()  (ASTContext.cpp)
Path 2 — C macro     : mulle_metaabi_is_voidptr_compatible_expression()
Path 3 — Encoding    : mulle_objc_signature_get_metaabiparamtype()

Decision Tree #

Return and param are classified independently, then combined. A struct/FP return forces STRUCT mode because the buffer must hold the return slot.

                    ┌──────────────┐
                    │ RETURN TYPE  │
                    └──────┬───────┘
                           │
        ┌──────────────────┼──────────────────────┐
        │                  │                      │
  ┌─────▼──────┐   ┌──────▼──────────┐   ┌───────▼──────────┐
  │    void    │   │ void*-compat    │   │ float / double / │
  │            │   │ (id, int, ptr,  │   │ long double /    │
  │            │   │  SEL, bool,     │   │ struct / union   │
  │            │   │  Class, ^block) │   │ (ANY size)       │
  └─────┬──────┘   └──────┬──────────┘   └───────┬──────────┘
        │                  │                     │
        │                  │               ═══► STRUCT ═══
        │                  │               rval in buffer,
        │                  │               all params in buffer too.
        │                  │               No further branching needed.
        │                  │
        ▼                  ▼
  ┌────────────┐   ┌────────────┐
  │  # PARAMS  │   │  # PARAMS  │
  └──┬───┬───┬─┘   └──┬───┬───┬─┘
     │   │   │        │   │   │
     0   1  2+        0   1  2+
     │   │   │        │   │   │
     │   │   │        │   │   │
     ▼   ▼   ▼        ▼   ▼   ▼
   VOID  ①  STR    VOID  ②  STR
         │   UC     _PTR  │   UC
         │    T     ③    │    T
         │                │
  ┌──────┴──────┐  ┌──────┴──────┐
  │ PARAM TYPE  │  │ PARAM TYPE  │
  └──┬──┬──┬──┬─┘  └──┬──┬──┬──┬─┘
     │  │  │  │       │  │  │  │
     ▼  ▼  ▼  ▼       ▼  ▼  ▼  ▼
     A  B  C  D       E  F  G  H

Leaf nodes — every real combination:

  ┌─────┬──────────────┬──────────────────┬──────────────────────────────────┐
  │Leaf │ Return       │ Param            │ Call pattern                     │
  ├─────┼──────────────┼──────────────────┼──────────────────────────────────┤
  │     │              │                  │                                  │
  │VOID │ void         │ 0 params         │ call(obj, sel, NULL)             │
  │     │              │                  │ (discard return)                 │
  │     │              │                  │                                  │
  │ A   │ void         │ 1 int/id/ptr/    │ call(obj, sel, (void*)val)       │
  │     │              │   SEL/bool       │ (discard return)                 │
  │     │              │                  │                                  │
  │ B   │ void         │ 1 float/double   │ buf = {param};                   │
  │     │              │                  │ call(obj, sel, &buf)             │
  │     │              │                  │ (discard return)                 │
  │     │              │                  │                                  │
  │ C   │ void         │ 1 tiny struct/   │ pack struct→void*;               │
  │     │              │   union ◄FIX►    │ call(obj, sel, packed)           │
  │     │              │                  │ (discard return)                 │
  │     │              │                  │                                  │
  │ D   │ void         │ 1 large struct/  │ buf = {param};                   │
  │     │              │   union          │ call(obj, sel, &buf)             │
  │     │              │                  │ (discard return)                 │
  │     │              │                  │                                  │
  │STRUC│ void         │ 2+ params        │ buf = {p1, p2, ...};             │
  │ T   │              │                  │ call(obj, sel, &buf)             │
  │     │              │                  │ (discard return)                 │
  ├─────┼──────────────┼──────────────────┼──────────────────────────────────┤
  │     │              │                  │                                  │
  │VOID │ int/id/ptr/  │ 0 params         │ rval = call(obj, sel, NULL)      │
  │_PTR │ SEL/bool     │                  │ result = (Type)rval              │
  │ ③  │              │                  │                                  │
  │     │              │                  │                                  │
  │ E   │ int/id/ptr/  │ 1 int/id/ptr/    │ rval = call(obj, sel, (void*)val)│
  │     │ SEL/bool     │   SEL/bool       │ result = (Type)rval              │
  │     │              │                  │                                  │
  │ F   │ int/id/ptr/  │ 1 float/double   │ buf = {param};                   │
  │     │ SEL/bool     │                  │ rval = call(obj, sel, &buf)      │
  │     │              │                  │ result = (Type)rval              │
  │     │              │                  │                                  │
  │ G   │ int/id/ptr/  │ 1 tiny struct/   │ pack struct→void*;               │
  │     │ SEL/bool     │   union ◄FIX►    │ rval = call(obj, sel, packed)    │
  │     │              │                  │ result = (Type)rval              │
  │     │              │                  │                                  │
  │ H   │ int/id/ptr/  │ 1 large struct/  │ buf = {param};                   │
  │     │ SEL/bool     │   union          │ rval = call(obj, sel, &buf)      │
  │     │              │                  │ result = (Type)rval              │
  │     │              │                  │                                  │
  │STRUC│ int/id/ptr/  │ 2+ params        │ buf = {p1, p2, ...};             │
  │ T   │ SEL/bool     │                  │ rval = call(obj, sel, &buf)      │
  │     │              │                  │ result = (Type)rval              │
  ├─────┼──────────────┼──────────────────┼──────────────────────────────────┤
  │     │              │                  │                                  │
  │STRUC│ float/double │ any              │ buf = {rval_slot, params...};    │
  │ T   │              │                  │ call(obj, sel, &buf)             │
  │     │              │                  │ result = *(FP_Type*)buf          │
  │     │              │                  │                                  │
  │STRUC│ struct/union │ any              │ buf = {rval_slot, params...};    │
  │ T   │ (any size)   │                  │ call(obj, sel, &buf)             │
  │     │              │                  │ result = *(StructType*)buf       │
  └─────┴──────────────┴──────────────────┴──────────────────────────────────┘

Note ③: -(id)self and -(int)count are VOID_PTR — _param is NULL but the return value is in the register and must be captured. This is distinct from VOID where the return is discarded.

1-param type classification (leaves A–H) #

  ┌──────────────────────────────────────────────────────────────────┐
  │ Param type              │ size ≤ void*? │ align ≤ void*? │ FP?   │
  │                         │               │                │       │
  │ int/id/ptr/SEL/bool     │ yes           │ yes            │ no    │
  │   → VOID_PTR (A/E)      │               │                │       │
  │                         │               │                │       │
  │ float/double/long double│ yes (64-bit)  │ yes            │ YES   │
  │   → STRUCT (B/F)        │               │                │       │
  │                         │               │                │       │
  │ tiny struct/union       │ yes           │ yes            │ n/a   │
  │   → VOID_PTR (C/G)      │ ◄── OUR FIX: Path 3 now checks this    │
  │                         │                                        │
  │ large struct/union      │ no            │ maybe no       │ n/a   │
  │   → STRUCT (D/H)        │                                        │
  └──────────────────────────────────────────────────────────────────┘

VOID_PTR — single void*-compatible param, non-struct/non-FP return #

  PACK PARAM into void*:

  ┌──────────────────────────┬─────────────────────────────────────────┐
  │ int / long / long long   │ (void *)(intptr_t) value                │
  │ bool                     │ (void *)(intptr_t) value                │
  │ id / Class               │ (void *) value                          │
  │ ptr / ^block / SEL       │ (void *) value                          │
  │ tiny struct or union     │ store into void*-sized tmp, reinterpret │
  │   e.g. {float x, float y}│   alloca tmp; store struct; load tmp    │
  └──────────────────────────┴─────────────────────────────────────────┘

  id result = call(obj, sel, packed);

  UNPACK RETURN from result:

  ┌──────────────────────────┬─────────────────────────────────────────┐
  │ void                     │ (discard result)                        │ 
  │ int / long / long long   │ (Type)(intptr_t) result                 │
  │ bool                     │ result != NULL                          │
  │ id / Class / ptr / SEL   │ (Type) result                           │
  └──────────────────────────┴─────────────────────────────────────────┘

Examples:


STRUCT — multi-param, FP param, large struct, or any struct/FP return #

The metaabi buffer layout depends on whether the return type needs a slot:

If return type is void, int, id, ptr (no struct-return slot):

  _param ──►  ┌─────────────────────────────┐
              │  param1  (aligned)          │
              ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
              │  param2  (aligned)          │
              ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
              │  ...                        │
              └─────────────────────────────┘

  call(obj, sel, (id)_param);
  rval via normal return register (id/void*)

If return type is float, double, struct, or union (struct-return slot at front):

  _param ──►  ┌─────────────────────────────┐
              │  return value slot          │  ◄── callee writes here
              │  (sizeof ReturnType bytes)  │  ◄── caller reads after call
              ├─────────────────────────────┤
              │  param1  (aligned)          │
              ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
              │  param2  (aligned)          │
              ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
              │  ...                        │
              └─────────────────────────────┘

  call(obj, sel, (id)_param);
  ReturnType result = *(ReturnType *)_param;   // read from front

Note: all params and the return slot are naturally aligned within the buffer.


Example Matrix #

  Encoding                           Return  Params        Mode     Leaf Notes
  ───────────────────────────────────────────────────────────────────────────────
  -(void)    reset                   void    0             VOID
  -(void)    setName:(id)s           void    1 id          VOID_PTR  A
  -(void)    setCount:(int)n         void    1 int         VOID_PTR  A
  -(void)    setFlag:(BOOL)b         void    1 bool        VOID_PTR  A
  -(void)    setFloat:(float)f       void    1 float       STRUCT    B   FP param
  -(void)    setDouble:(double)d     void    1 double      STRUCT    B   FP param
  -(void)    move:(Point)p  {ff} 8B  void    1 tiny-struct VOID_PTR  C ◄ FIXED
  -(void)    setRect:(Rect)r {ffff}  void    1 large-struc STRUCT    D   >8 bytes
  -(void)    setXY:(int)x y:(int)y   void    2 int+int     STRUCT        2 params
  -(void)    foo:(float)f b:(float)g void    2 float+float STRUCT        2 params
  -(void)    foo:(Point)a b:(Point)b void    2 tiny+tiny   STRUCT        2 params
  ───────────────────────────────────────────────────────────────────────────────
  -(id)      self                    id      0             VOID_PTR  ③   no param
  -(id)      objectAt:(int)i         id      1 int         VOID_PTR  E
  -(id)      wrap:(Point)p  {ff} 8B  id      1 tiny-struct VOID_PTR  G ◄ FIXED
  -(int)     count                   int     0             VOID_PTR  ③   no param
  -(int)     indexOf:(id)o           int     1 id          VOID_PTR  E
  -(int)     add:(int)a b:(int)b     int     2 int+int     STRUCT        2 params
  ───────────────────────────────────────────────────────────────────────────────
  -(float)   val                     float   0             STRUCT        FP return
  -(float)   dot:(Point)p            float   1 tiny-struct STRUCT        FP return
  -(float)   dot:(Point)a b:(Point)b float   2 tiny+tiny   STRUCT        FP return
  -(double)  area:(Rect)r            double  1 large-struc STRUCT        FP return
  ───────────────────────────────────────────────────────────────────────────────
  -(Point)   origin         {ff} 8B  tiny-s  0             STRUCT        struct rval
  -(Point)   scaled:(float)f         tiny-s  1 float       STRUCT        struct rval
  -(Point)   add:(Point)a b:(Point)b tiny-s  2 tiny+tiny   STRUCT        struct rval
  -(CGRect)  bounds         {ffff}   large-s 0             STRUCT        struct rval
  -(NSRange) range:(int)loc {QQ} 16B large-s 1 int         STRUCT        struct rval
  ───────────────────────────────────────────────────────────────────────────────

Key insight: the return type is tested first and can force STRUCT mode regardless of the parameter types. A method like -(Point) mirror:(Point)p goes STRUCT because the return type is a struct — not because of the param.


Why Return Structs Always Force STRUCT Mode #

Even a tiny struct return (sizeof == 4) goes through struct-buffer mode. The caller allocates the buffer, passes a pointer in _param, and the callee writes the return value into _param[0].

This asymmetry vs params (tiny struct param → VOID_PTR) exists because:


Why FP Params Force STRUCT Mode (even if ≤ sizeof(void*)) #

A float is 4 bytes, well under 8, but packing it into a void* via a union type-pun causes a store-forwarding stall on x86:

  ; union pun — STALL (write 4 bytes FP, read 8 bytes GP from same addr)
  movss   %xmm0, (%rsp)    ; 4-byte float store (SSE)
  movq    (%rsp), %rdi     ; 8-byte GP load     ← CPU stall ~5–20 cycles

  ; struct-buffer — no stall (just pass the address)
  movss   %xmm0, 8(%rsp)   ; store float into alloca'd slot
  leaq    8(%rsp), %rdi    ; pass address — no memory reload at all

Benchmarked: ~35% faster at 100M iterations (-O2, 3 separate TUs).


Why Tiny Struct Params DO Use VOID_PTR (no FP poison) #

A struct with float members, e.g. {float x, float y}, does NOT trigger the FP store-forwarding hazard because:


FP Return vs Tiny-Struct Return Asymmetry #

  Type          As param      As return
  ──────────────────────────────────────
  float         STRUCT        STRUCT
  double        STRUCT        STRUCT
  {ff} Point    VOID_PTR ◄FIX STRUCT
  {QQ} NSRange  STRUCT        STRUCT   (large, >void*)
  id            VOID_PTR      VOID_PTR
  int           VOID_PTR      VOID_PTR

The fix makes {ff} Point as a param behave like int (VOID_PTR), while leaving {ff} Point as a return value going through STRUCT — exactly matching the compiler's typeNeedsMetaABIAlloca(isParam=true/false).

last updated: