On 15 Oct 2026, at 05:35, Jeff Law <[email protected]> wrote:
On 9/22/26 3:33 AM, [email protected] wrote:
From: Kyrylo Tkachov <[email protected]>
An RTL basic block that ends in a tablejump cannot normally be duplicated.
The instructions that compute the table address can be outside the block, so
copying and retargeting the table is not reliable.
Let an opted-in pass keep one table instead. Before duplication, one jump
owns the adjacent table:
jump A -> label T -> addr_diff_vec
After duplication, both jumps name the same label and table:
jump A --\
label T -> addr_diff_vec
jump B --/
Mark the table with the sticky JUMP_TABLE_SHARED_P flag. A shared table is
immutable because each entry describes a successor of every dispatch that
uses it. Block deletion can remove one dispatch but must retain the table for
the others.
Add PROP_rtl_allow_tablejump_duplication as a function property. The RTL hook
uses it to permit the copy, retain JUMP_LABEL, and mark the table. A pass can
set the property while it is prepared for the table to become immutable.
Guard the RTL redirection, deletion, fall-through, merge, and cross-jump paths
that assume one owner or physical adjacency. Add checking assertions for
bulk deletion and verify every shared dispatch against the table successors.
Treat shared tablejumps as nonregular for interblock scheduling. This prevents
selective scheduling from splitting shared-table edges to create loop
preheaders or bookkeeping blocks.
No pass opts in in this patch, so it does not change generated code by
itself.
gcc/ChangeLog:
PR middle-end/126236
* tree-pass.h (PROP_rtl_allow_tablejump_duplication): New property.
* cfgcleanup.cc (outgoing_edges_match): Reject matches involving a
shared jump table.
* cfgrtl.cc (shared_jump_table_component_p): New function.
(shared_jump_table_between_p): Likewise.
(delete_insn_chain): Assert that the range does not contain a shared
jump table.
(can_fallthru, rtl_can_merge_blocks, rtl_tidy_fallthru_edge): Do not
remove or cross a shared jump table between basic blocks.
(cfg_layout_can_duplicate_bb_p): Allow a tablejump when the new
property is set.
(duplicate_insn_chain): Retain a copied tablejump's table and mark it
shared.
(patch_jump_insn): Refuse to redirect through a shared table.
(try_redirect_by_replacing_jump): Do not delete a shared table.
(get_last_bb_insn): Do not treat a shared table as part of one block.
(force_nonfallthru_and_redirect): Likewise.
(rtl_verify_edges): Check each shared table against the successors of
each dispatch that uses it.
* jump.cc (delete_related_insns): Do not delete a shared table.
* rtl.h (JUMP_TABLE_SHARED_P): New macro.
(rtx_def): Document the shared-table use of the unchanging flag.
* sched-rgn.cc (is_cfg_nonregular): Reject shared tablejumps.
* doc/cfg.texi: Document shared jump tables.
* doc/md.texi: Likewise.
* doc/rtl.texi: Likewise.
Signed-off-by: Kyrylo Tkachov <[email protected]>
---
So high level question. For ADDR_DIFF_VEC, I think we also have to be careful
that the implementation doesn't use a pc-relative jump under the hood. The PA
used to work like that until circa 2013 (I didn't realize it'd been rewritten
until I started looking at it's code and didn't see what I expected to see). I
can't speak for other ports. I probably should have included this in the audit
request I did the first time around. However the PA may be using mfia rather
than a blr which may result in it still being pc-relative, just not in the way
I expected to find.
I think the assumption is the we're doing a pc-relative jump using the table
element to get hte difference between the jump site and the final label (with
some minor delta). That has worked because the table follows the pc-relative
indirect jump immediately in the insn stream. I don't think it's going to work
if we share jump tables on targets that use ADDR_DIFF_VEC.
Yes. On aarch64 its ADDR_DIFF_VEC entries are (case - .LrtxN) / 4, and
aarch64_output_casesi defines .LrtxN right after the dispatch. AArch64 opts in
with TARGET_ASM_JUMP_TABLE_BASE which emits .LrtxN once at the position of the
table label.
Since I haven’t defined the hook for other targets, this is effectively the
opt-in mechanism that only aarch64 implements for now.
I would look very closely at cris, vax, sh, stormy16, hppa, c6x, nds32, s390,
ft32, mips (16), arm/thumb2, csky, mn103 and csky. Using pc-relative in and of
itself isn't a problem, but instead a signal that there may be some implicit
dependency on the jump table immediately following the jump itself.
So with the help of some LLM agents to do the digging…
If sharing were the default, these would have to opt out completely or for some
configs:
- cris: *casesi_adds_w is "adds.w [$pc+%0.w],$pc". It reads the entry
at its own PC and adds it to that PC. cris_asm_output_case_end also
asserts that a jump directly precedes the table label.
- vax: casel has no table operand. The displacement table is part of
the instruction.
- sh: from SH2 up the dispatch is casesi_jump_2 (braf). For each table,
fixup_addr_diff_vecs finds the nearest casesi_jump_2 before it, emits
the braf reference label after that jump and rebases the ADDR_DIFF_VEC
on that label. All other copies would branch to the wrong place. The
SH1 casesi_jump_1 computes the right target from anywhere, but sh_reorg
expects one mova per table, and for SH1 PIC its mova fixup emits the
SH2-only casesi_worker_2.
- stormy16: tablejump_pcrel is "br %0" into a table of jmpf instructions
that has no label and must follow the br. This table is an ADDR_VEC,
so a check for ADDR_DIFF_VEC would not catch it.
- arm: Thumb-2 uses tbb/tbh [pc, rN] for QImode and HImode tables. The
Thumb-1 -fpic helpers take the table address from LR. Arm state
defines .LrtxN after each dispatch and, except for non-PIC SImode
tables, adds its own PC. The Thumb-2 SImode form loads the table label
with adr, so it could share within the 4 KiB adr range. But
shorten_branches chooses the table mode after duplication, so a hook
cannot select that form. Also, the arm_reorg minipool scan jumps from
each tablejump to its table. It would not terminate on a copy after
the table, and for a copy before the table it skips the code between
them.
- hppa: The blr form (casesi0) stopped being the
default in 2004 (-mbig-switch) and was removed in 2013. casesi32
(32-bit non-PIC) loads the table label with ldil/ldo and uses an
ADDR_VEC, which is fine. casesi32p and casesi64p get the PC with
"bl .+8" or mfia and add the distance to the table label with one ldo.
Until PR target/40575 (2009) that ldo used a fixed offset, so it
assumed adjacency. Now the ldo uses the table label and the entries
are relative to it. Only the ldo reach remains: +-8 KiB on 32-bit and
+-32 KiB on 64-bit. Today only adjacency guarantees it. So PIC, which
includes all 64-bit code, cannot share unless these patterns get a long
form like the pic_label_operand patterns have.
- c6x: the PIC casesi_internal is similar. Its addkpc of the table label
reaches only about 256 bytes, and c6x has no length attribute to check
the distance. The non-PIC tablejump uses mvkl/mvkh and an absolute
ADDR_VEC, which is fine.
- mips: MIPS16 with the default -mcode-readable=yes uses
casesi_internal_mips16, whose la of the table label is an ADDIUPC.
Its insn_count assumes the short form, which only adjacency guarantees.
A copy after the table, or more than 1020 bytes before it (124 bytes
for the 64-bit dla), is longer than GCC thinks. Beyond +-32 KiB the la
can fail to assemble. tablejump_<mode> (normal MIPS, microMIPS, and
MIPS16 with -mcode-readable=pcrel or no) is a register jump with the
table in .rodata, which is fine.
- csky: at -O2 the dispatch is *tablejump, a register jmp with the table
in .rodata, which is fine. The casesi helpers that use LR as the table
base are only used with optimize_size. But the -mconstpool minipool
scan in csky_reorg is based on the ARM one and has the same problem.
-mconstpool is mandatory on CK801 and CK802.
Also:
- m68k uses
jmp (2,pc,dN.w) (see PR target/112413).
- arc with -mbranch-index and -mcode-density uses bi/bih.
- rx with -mpid or -fpic has entries relative to the "1:" label that each
dispatch emits.
These are fine:
- nds32: casesi_internal loads the table label into $ta in each jump,
also for PIC, and adds the .Lcase-.Ltable entry to $ta, not to the PC.
"pc relative" in the names means relative to the table label.
- s390: casesi loads the table with larl of the table label, the entries
are absolute or relative to the table label, and the tables are in
.rodata. The thunk-inline forms have a private label, but
s390_cannot_copy_insn_p already stops them from being copied. An
earlier s390x qemu run with an audit-only hook passed. It needed a
larger duplication budget, because the default made no copies.
- ft32: casesi0 loads the table label with an absolute ldk.l, and the
entries are absolute.
- mn10300: casesi loads the table label with a move (or @GOTOFF plus
a2), the tables are in .rodata and the jump is "jmp (An)".
More mainstream targets like x86, PowerPC, RISC-V and s390 should be fine.
So TL;DR, I think we can make it default-on and opt-out for targets, but we’ll
need to implement the opt out for a bunch of these targets/sub-targets up front.