On 10/15/26 9:34 AM, Kyrylo Tkachov wrote:

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.
THanks.  I'd lean towards the opt-out approach -- it would both signal the new way is preferred and give it a bit more coverage via the automated testers.  But I also realize it's more work for you, so I won't require it -- your call.

Thanks,
jeff

Reply via email to