Skip to content

chore: use one static dispatch table for nightly tail calls - #55

Open
stevefan1999-personal wants to merge 1 commit into
explodingcamera:nextfrom
stevefan1999-personal:patch-efficient-tail
Open

chore: use one static dispatch table for nightly tail calls#55
stevefan1999-personal wants to merge 1 commit into
explodingcamera:nextfrom
stevefan1999-personal:patch-efficient-tail

Conversation

@stevefan1999-personal

Copy link
Copy Markdown

I noticed that compiling with explicit TCO, each tail call will create the jump table for each handler independently, causing a simple Hello World example to go to 14MB. I immediately disable the inline(always) for the handler but I still noticed that each handler still have their own threaded jump table too. So I did it further and used a common one-time static opcode table for a full indirect threading implementation.

Benchmark-wise I did not see any significant regression, but the binary size did shrink from 14MB to 560KB on my system

@explodingcamera

Copy link
Copy Markdown
Owner

Thanks for the PR! I tested this on my machine, and the growth seems to happen only with fat LTO, at least on Linux x86_64. Changing only handler_for to #[inline(never)] fixed it for me, and I couldn’t find any other duplicated jump tables. Could you share which target and Rust version you tested with?

@stevefan1999-personal

stevefan1999-personal commented Sep 1, 2026

Copy link
Copy Markdown
Author

Thanks for the PR! I tested this on my machine, and the growth seems to happen only with fat LTO, at least on Linux x86_64. Changing only handler_for to #[inline(never)] fixed it for me, and I couldn’t find any other duplicated jump tables. Could you share which target and Rust version you tested with?

Latest nightly 1.100.0. I did further and go for a more optimized indirect threaded code design.

Code should look like this:

text:000000014002FDFE ; void __fastcall sub_14002FDFE(__int64, __int64)
.text:000000014002FDFE sub_14002FDFE   proc near               ; CODE XREF: sub_14000F407+CC↑p
.text:000000014002FDFE                                         ; sub_14000F535+78↑j ...
.text:000000014002FDFE                 push    rsi             ; jumptable 000000
.text:000000014002FDFE                                         ; jumptable 000000014000F5AD case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F631 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F6C1 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F729 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F791 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F7F9 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F86A case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F8E0 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F952 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000F9D2 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000FAC9 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000FBF4 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000FEA0 case 464
.text:000000014002FDFE                                         ; jumptable 000000014000FFD0 case 464
.text:000000014002FDFE                                         ; jumptable 000000014001009D case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010196 case 464
.text:000000014002FDFE                                         ; jumptable 000000014001029B case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010370 case 464
.text:000000014002FDFE                                         ; jumptable 00000001400105D9 case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010748 case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010A12 case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010B14 case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010C14 case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010D12 case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010E0E case 464
.text:000000014002FDFE                                         ; jumptable 0000000140010F3C case 464
.text:000000014002FDFE                                         ; jumptable 0000000140011073 case 464
.text:000000014002FDFE                                         ; jumptable 000000014001130C case 464
.text:000000014002FDFF                 sub     rsp, 20h
.text:000000014002FE03                 mov     rax, [rcx+30h]
.text:000000014002FE07                 mov     r8, [rax+2B8h]
.text:000000014002FE0E                 test    r8, r8
.text:000000014002FE11                 jz      loc_14002FEAB
.text:000000014002FE17                 mov     r9, r8
.text:000000014002FE1A                 dec     r9
.text:000000014002FE1D                 jz      loc_14002FEAB
.text:000000014002FE23                 mov     r10, [rax+2B0h]
.text:000000014002FE2A                 mov     r11, r9
.text:000000014002FE2D                 shl     r11, 4
.text:000000014002FE31                 vmovsd  xmm0, qword ptr [r10+r11]
.text:000000014002FE37                 vmovsd  xmm1, qword ptr [r10+r11+8]
.text:000000014002FE3E                 shl     r8, 4
.text:000000014002FE42                 xor     r11d, r11d
.text:000000014002FE45                 vucomisd xmm0, qword ptr [r8+r10-20h]
.text:000000014002FE4C                 setnbe  r11b
.text:000000014002FE50                 neg     r11
.text:000000014002FE53                 xor     esi, esi
.text:000000014002FE55                 vucomisd xmm1, qword ptr [r8+r10-18h]
.text:000000014002FE5C                 setnbe  sil
.text:000000014002FE60                 neg     rsi
.text:000000014002FE63                 mov     [r8+r10-20h], r11
.text:000000014002FE68                 mov     [r8+r10-18h], rsi
.text:000000014002FE6D                 mov     [rax+2B8h], r9
.text:000000014002FE74                 lea     rax, [rdx+1]
.text:000000014002FE78                 mov     r8, [rcx+20h]
.text:000000014002FE7C                 cmp     rax, [r8+18h]
.text:000000014002FE80                 jnb     short def_14002FEA7 ; jumptable 000000014002FEA7 default case
.text:000000014002FE82                 mov     r8, [r8+10h]
.text:000000014002FE86                 mov     r8, [r8+rdx*8+8]
.text:000000014002FE8B                 movzx   r9d, r8w
.text:000000014002FE8F                 cmp     r9, 272h        ; switch 626 cases
.text:000000014002FE96                 jnb     short def_14002FEA7 ; jumptable 000000014002FEA7 default case
.text:000000014002FE98                 lea     r10, funcs_14000F4D3
.text:000000014002FE9F                 mov     rdx, rax
.text:000000014002FEA2                 add     rsp, 20h
.text:000000014002FEA6                 pop     rsi
.text:000000014002FEA7                 jmp     ds:(funcs_14000F4D3 - 14009C200h)[r10+r9*8] ; switch jump
.text:000000014002FEAB ; ---------------------------------------------------------------------------
.text:000000014002FEAB
.text:000000014002FEAB loc_14002FEAB:                          ; CODE XREF: sub_14002FDFE+13↑j
.text:000000014002FEAB                                         ; sub_14002FDFE+1F↑j
.text:000000014002FEAB                 call    sub_140086317
.text:000000014002FEB0 ; ---------------------------------------------------------------------------
.text:000000014002FEB0
.text:000000014002FEB0 def_14002FEA7:                          ; CODE XREF: sub_14002FDFE+82↑j
.text:000000014002FEB0                                         ; sub_14002FDFE+98↑j
.text:000000014002FEB0                 ud2                     ; jumptable 000000014002FEA7 default case
.text:000000014002FEB0 sub_14002FDFE   endp

Notice the:

jmp     ds:(funcs_14000F4D3 - 14009C200h)[r10+r9*8] ; switch jump

Which is the computed goto that directly dispatch on the next opcode

@stevefan1999-personal

stevefan1999-personal commented Sep 1, 2026

Copy link
Copy Markdown
Author

Thanks for the PR! I tested this on my machine, and the growth seems to happen only with fat LTO, at least on Linux x86_64. Changing only handler_for to #[inline(never)] fixed it for me, and I couldn’t find any other duplicated jump tables. Could you share which target and Rust version you tested with?

Well, you still have dedicated match branches which is another jump tables, but it can be disabled by gcc/clang to prevent jump branch table optimization. So I further improved it to have an explicitly shared opcode table, because that opcode match branch, far as I remember, is still duplicated, because you still need an index to the jump table, and the size shrinked ~18KB far as I remember.

The idea is like:

let next_op = match next_ip {
   Opcode::Add => add,
  ...
};
become next_op(...);

Far as I remember too, each handler will duplicate this pattern and the match branch.

Now it becomes:

const OPCODE: [usize; N] = [
  &add,
  ...
];

become OPCODE[next_ip](...);

So while it technically should be optimized as identical, I don't think this is the case today.

So it is originally around 579KB with the #[inline(never)] trick, then I further did it with a proper indirect threaded code design, which now goes down to 560KB.

@explodingcamera

Copy link
Copy Markdown
Owner

I think my #[inline(always)] there was definitely a mistake, so I think I'll just remove that separately from this PR. I was a bit confused since on Linux the opcode table resulted in a slightly larger binary than my match (when not inline), which LLVM already appears to lower to a shared lookup table.

I'm also a little hesitant to add more unstable features and unsafe code for this. I might look into exposing the opcode safely through the instruction representation instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants