A load pair that partially overlaps a recent scalar store can prevent the
load from using store-to-load forwarding. Splitting the pair lets the
overlapping half use the forwarding path.
Add a late AArch64 RTL pass that handles non-writeback SI and DI GPR load
pairs. Search a bounded part of the containing basic block for a scalar
store to either half, and replace the pair only when both scalar loads are
recognized. Reject volatile, frame-related and exception-sensitive forms,
and disable the transformation for size optimization. Keep the pass off by
default behind a target parameter while its profitability is evaluated.
gcc/ChangeLog:
* config.gcc (aarch64*-*-*): Add aarch64-ldp-split.o.
* config/aarch64/aarch64-ldp-split.cc: New file.
* config/aarch64/aarch64-passes.def: Insert pass_split_ldp_stlf before
pass_sched2.
* config/aarch64/aarch64-protos.h (make_pass_split_ldp_stlf): Declare.
* config/aarch64/aarch64.opt: Add aarch64-split-ldp-stlf parameter.
* config/aarch64/t-aarch64 (aarch64-ldp-split.o): New rule.
* doc/params.texi (aarch64-split-ldp-stlf): Document.
gcc/testsuite/ChangeLog:
* gcc.target/aarch64/split-ldp-stlf-1.c: New test.
* gcc.target/aarch64/split-ldp-stlf-2.c: New test.
* gcc.target/aarch64/split-ldp-stlf-3.c: New test.
* gcc.target/aarch64/split-ldp-stlf-4.c: New test.
* gcc.target/aarch64/split-ldp-stlf-5.c: New test.
* gcc.target/aarch64/split-ldp-stlf-6.c: New test.
Signed-off-by: huzife <[email protected]>
---
gcc/config.gcc | 1 +
gcc/config/aarch64/aarch64-ldp-split.cc | 316 ++++++++++++++++++
gcc/config/aarch64/aarch64-passes.def | 1 +
gcc/config/aarch64/aarch64-protos.h | 1 +
gcc/config/aarch64/aarch64.opt | 5 +
gcc/config/aarch64/t-aarch64 | 6 +
gcc/doc/params.texi | 8 +
.../gcc.target/aarch64/split-ldp-stlf-1.c | 43 +++
.../gcc.target/aarch64/split-ldp-stlf-2.c | 13 +
.../gcc.target/aarch64/split-ldp-stlf-3.c | 22 ++
.../gcc.target/aarch64/split-ldp-stlf-4.c | 14 +
.../gcc.target/aarch64/split-ldp-stlf-5.c | 13 +
.../gcc.target/aarch64/split-ldp-stlf-6.c | 13 +
13 files changed, 456 insertions(+)
create mode 100644 gcc/config/aarch64/aarch64-ldp-split.cc
create mode 100644 gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-1.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-2.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-3.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-4.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-5.c
create mode 100644 gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-6.c
diff --git a/gcc/config.gcc b/gcc/config.gcc
index 0477bf22c8a..00aa965f162 100644
--- a/gcc/config.gcc
+++ b/gcc/config.gcc
@@ -362,6 +362,7 @@ aarch64*-*-*)
extra_objs="${extra_objs} aarch-bti-insert.o"
extra_objs="${extra_objs} aarch64-early-ra.o"
extra_objs="${extra_objs} aarch64-ldp-fusion.o"
+ extra_objs="${extra_objs} aarch64-ldp-split.o"
extra_objs="${extra_objs} aarch64-sched-dispatch.o"
extra_objs="${extra_objs} aarch64-json-tunings-printer.o"
extra_objs="${extra_objs} aarch64-json-tunings-parser.o"
diff --git a/gcc/config/aarch64/aarch64-ldp-split.cc
b/gcc/config/aarch64/aarch64-ldp-split.cc
new file mode 100644
index 00000000000..a4c2c159529
--- /dev/null
+++ b/gcc/config/aarch64/aarch64-ldp-split.cc
@@ -0,0 +1,316 @@
+/* Split selected load pairs to improve store-to-load forwarding on AArch64.
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GCC.
+
+ GCC is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3, or (at your option) any later
+ version.
+
+ GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with GCC; see the file COPYING3. If not see
+ <http://www.gnu.org/licenses/>. */
+
+#define IN_TARGET_CODE 1
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "backend.h"
+#include "rtl.h"
+#include "df.h"
+#include "memmodel.h"
+#include "emit-rtl.h"
+#include "tree-pass.h"
+#include "insn-attr.h"
+#include "insn-constants.h"
+#include "insn-config.h"
+#include "recog.h"
+#include "cfgrtl.h"
+#include "predict.h"
+
+namespace {
+
+/* A non-writeback GPR load pair that is safe for this pass to split. */
+struct ldp_info
+{
+ rtx dests[2];
+ rtx mem;
+ rtx base;
+ machine_mode mode;
+ HOST_WIDE_INT offset;
+ HOST_WIDE_INT access_size;
+};
+
+/* Return true if REG is a hard general-purpose register in MODE. */
+
+static bool
+gpr_operand_p (rtx reg, machine_mode mode)
+{
+ return (REG_P (reg)
+ && HARD_REGISTER_P (reg)
+ && GP_REGNUM_P (REGNO (reg))
+ && GET_MODE (reg) == mode);
+}
+
+/* Extract the MEM from one half of an AArch64 load pair. */
+
+static rtx
+extract_ldp_mem (rtx set, rtx dest, int unspec_code)
+{
+ if (GET_CODE (set) != SET || !rtx_equal_p (SET_DEST (set), dest))
+ return NULL_RTX;
+
+ rtx src = SET_SRC (set);
+ if (GET_CODE (src) != UNSPEC
+ || XINT (src, 1) != unspec_code
+ || XVECLEN (src, 0) != 1)
+ return NULL_RTX;
+
+ rtx mem = XVECEXP (src, 0, 0);
+ return MEM_P (mem) ? mem : NULL_RTX;
+}
+
+/* If INSN is an ordinary, non-writeback SI or DI GPR LDP, describe it in
+ INFO and return true. Deliberately reject FP/SIMD, TImode, volatile,
+ frame-related, and EH-sensitive forms. */
+
+static bool
+extract_ldp (rtx_insn *insn, ldp_info *info)
+{
+ if (!NONDEBUG_INSN_P (insn)
+ || INSN_CODE (insn) < 0
+ || get_attr_ldpstp (insn) != LDPSTP_LDP
+ || RTX_FRAME_RELATED_P (insn)
+ || find_reg_note (insn, REG_EH_REGION, NULL_RTX))
+ return false;
+
+ rtx pattern = PATTERN (insn);
+ if (GET_CODE (pattern) != PARALLEL || XVECLEN (pattern, 0) != 2)
+ return false;
+
+ rtx sets[2] = { XVECEXP (pattern, 0, 0), XVECEXP (pattern, 0, 1) };
+ if (GET_CODE (sets[0]) != SET || GET_CODE (sets[1]) != SET)
+ return false;
+
+ info->dests[0] = SET_DEST (sets[0]);
+ info->dests[1] = SET_DEST (sets[1]);
+ info->mode = GET_MODE (info->dests[0]);
+ if ((info->mode != SImode && info->mode != DImode)
+ || !gpr_operand_p (info->dests[0], info->mode)
+ || !gpr_operand_p (info->dests[1], info->mode)
+ || reg_overlap_mentioned_p (info->dests[0], info->dests[1]))
+ return false;
+
+ rtx mem0 = extract_ldp_mem (sets[0], info->dests[0], UNSPEC_LDP_FST);
+ rtx mem1 = extract_ldp_mem (sets[1], info->dests[1], UNSPEC_LDP_SND);
+ if (!mem0
+ || !mem1
+ || !rtx_equal_p (mem0, mem1)
+ || MEM_VOLATILE_P (mem0)
+ || side_effects_p (XEXP (mem0, 0))
+ || !MEM_SIZE_KNOWN_P (mem0)
+ || !MEM_SIZE (mem0).is_constant ())
+ return false;
+
+ info->access_size = GET_MODE_SIZE (info->mode).to_constant ();
+ if (!known_eq (MEM_SIZE (mem0), 2 * info->access_size))
+ return false;
+
+ poly_int64 offset;
+ info->base = strip_offset (XEXP (mem0, 0), &offset);
+ if (!REG_P (info->base) || !offset.is_constant (&info->offset))
+ return false;
+
+ info->mem = mem0;
+ return true;
+}
+
+/* Return true if INSN is a nonvolatile GPR store of one LDP half, using the
+ same unmodified base as INFO and writing exactly the low or high half. */
+
+static bool
+forwarding_store_p (rtx_insn *insn, const ldp_info &info)
+{
+ if (!NONDEBUG_INSN_P (insn) || RTX_FRAME_RELATED_P (insn))
+ return false;
+
+ rtx set = single_set (insn);
+ if (!set || !MEM_P (SET_DEST (set)))
+ return false;
+
+ rtx mem = SET_DEST (set);
+ rtx value = SET_SRC (set);
+ if (GET_MODE (mem) != info.mode
+ || !gpr_operand_p (value, info.mode)
+ || MEM_VOLATILE_P (mem)
+ || side_effects_p (XEXP (mem, 0))
+ || !MEM_SIZE_KNOWN_P (mem)
+ || !known_eq (MEM_SIZE (mem), info.access_size))
+ return false;
+
+ poly_int64 poly_offset;
+ rtx base = strip_offset (XEXP (mem, 0), &poly_offset);
+ HOST_WIDE_INT offset;
+ if (!REG_P (base)
+ || !rtx_equal_p (base, info.base)
+ || !poly_offset.is_constant (&offset))
+ return false;
+
+ return (offset == info.offset
+ || offset == info.offset + info.access_size);
+}
+
+/* Search backwards in the containing basic block for a nearby store that
+ makes splitting PAIR profitable. Stop once the base value changes. */
+
+static rtx_insn *
+find_forwarding_store (rtx_insn *pair, const ldp_info &info)
+{
+ basic_block bb = BLOCK_FOR_INSN (pair);
+ unsigned int distance = 0;
+ unsigned int max_distance = (unsigned int) aarch64_split_ldp_stlf;
+
+ for (rtx_insn *insn = PREV_INSN (pair);
+ insn && BLOCK_FOR_INSN (insn) == bb;
+ insn = PREV_INSN (insn))
+ {
+ if (!NONDEBUG_INSN_P (insn))
+ continue;
+
+ if (++distance > max_distance)
+ break;
+
+ if (CALL_P (insn) || modified_in_p (info.base, insn))
+ break;
+
+ if (forwarding_store_p (insn, info))
+ return insn;
+ }
+
+ return NULL;
+}
+
+/* Replace PAIR with two individually-recognized scalar loads. Load the
+ non-base destination first when the low destination overlaps the base. */
+
+static bool
+split_ldp (rtx_insn *pair, const ldp_info &info)
+{
+ rtx mems[2] = {
+ adjust_address_nv (info.mem, info.mode, 0),
+ adjust_address_nv (info.mem, info.mode, info.access_size)
+ };
+
+ int order[2] = { 0, 1 };
+ if (reg_overlap_mentioned_p (info.base, info.dests[0]))
+ std::swap (order[0], order[1]);
+
+ start_sequence ();
+ emit_insn (gen_rtx_SET (info.dests[order[0]], mems[order[0]]));
+ emit_insn (gen_rtx_SET (info.dests[order[1]], mems[order[1]]));
+ rtx_insn *seq = get_insns ();
+ end_sequence ();
+
+ unshare_all_rtl_in_chain (seq);
+ for (rtx_insn *insn = seq; insn; insn = NEXT_INSN (insn))
+ if (!NONDEBUG_INSN_P (insn) || recog_memoized (insn) < 0)
+ {
+ if (dump_file)
+ fprintf (dump_file,
+ "Rejecting LDP %d: scalar replacement is not recognized\n",
+ INSN_UID (pair));
+ return false;
+ }
+
+ if (dump_file)
+ {
+ fprintf (dump_file, "Splitting LDP %d into:\n", INSN_UID (pair));
+ for (rtx_insn *insn = seq; insn; insn = NEXT_INSN (insn))
+ print_rtl_single (dump_file, insn);
+ }
+
+ emit_insn_before_setloc (seq, pair, INSN_LOCATION (pair));
+ delete_insn (pair);
+ return true;
+}
+
+const pass_data pass_data_split_ldp_stlf =
+{
+ RTL_PASS, /* type. */
+ "split_ldp_stlf", /* name. */
+ OPTGROUP_NONE, /* optinfo_flags. */
+ TV_MACH_DEP, /* tv_id. */
+ 0, /* properties_required. */
+ 0, /* properties_provided. */
+ 0, /* properties_destroyed. */
+ 0, /* todo_flags_start. */
+ TODO_df_verify | TODO_df_finish, /* todo_flags_finish. */
+};
+
+class pass_split_ldp_stlf : public rtl_opt_pass
+{
+public:
+ pass_split_ldp_stlf (gcc::context *ctxt)
+ : rtl_opt_pass (pass_data_split_ldp_stlf, ctxt)
+ {}
+
+ bool gate (function *fn) final override
+ {
+ return (optimize >= 1
+ && !optimize_debug
+ && reload_completed
+ && aarch64_split_ldp_stlf
+ && !optimize_function_for_size_p (fn)
+ && !fn->can_throw_non_call_exceptions);
+ }
+
+ unsigned int execute (function *fn) final override
+ {
+ unsigned int split_count = 0;
+ basic_block bb;
+
+ FOR_EACH_BB_FN (bb, fn)
+ {
+ rtx_insn *insn;
+ rtx_insn *next;
+ FOR_BB_INSNS_SAFE (bb, insn, next)
+ {
+ ldp_info info;
+ if (!extract_ldp (insn, &info))
+ continue;
+
+ rtx_insn *store = find_forwarding_store (insn, info);
+ if (!store)
+ continue;
+
+ if (dump_file)
+ {
+ fprintf (dump_file, "Found forwarding store for LDP %d:\n",
+ INSN_UID (insn));
+ print_rtl_single (dump_file, store);
+ }
+
+ split_count += split_ldp (insn, info);
+ }
+ }
+
+ if (dump_file)
+ fprintf (dump_file, "Split %u LDP instructions\n", split_count);
+ return 0;
+ }
+};
+
+} // namespace
+
+rtl_opt_pass *
+make_pass_split_ldp_stlf (gcc::context *ctxt)
+{
+ return new pass_split_ldp_stlf (ctxt);
+}
diff --git a/gcc/config/aarch64/aarch64-passes.def
b/gcc/config/aarch64/aarch64-passes.def
index 2df7459af92..594348a0532 100644
--- a/gcc/config/aarch64/aarch64-passes.def
+++ b/gcc/config/aarch64/aarch64-passes.def
@@ -26,4 +26,5 @@ INSERT_PASS_BEFORE (pass_late_thread_prologue_and_epilogue,
1, pass_late_track_s
INSERT_PASS_BEFORE (pass_shorten_branches, 1, pass_insert_bti);
INSERT_PASS_BEFORE (pass_early_remat, 1, pass_ldp_fusion);
INSERT_PASS_BEFORE (pass_peephole2, 1, pass_ldp_fusion);
+INSERT_PASS_BEFORE (pass_sched2, 1, pass_split_ldp_stlf);
INSERT_PASS_BEFORE (pass_free_cfg, 1, pass_narrow_gp_writes);
diff --git a/gcc/config/aarch64/aarch64-protos.h
b/gcc/config/aarch64/aarch64-protos.h
index bcc833cfaa1..ec8be386c29 100644
--- a/gcc/config/aarch64/aarch64-protos.h
+++ b/gcc/config/aarch64/aarch64-protos.h
@@ -1268,6 +1268,7 @@ rtl_opt_pass *make_pass_late_track_speculation
(gcc::context *);
rtl_opt_pass *make_pass_insert_bti (gcc::context *ctxt);
rtl_opt_pass *make_pass_switch_pstate_sm (gcc::context *ctxt);
rtl_opt_pass *make_pass_ldp_fusion (gcc::context *);
+rtl_opt_pass *make_pass_split_ldp_stlf (gcc::context *);
rtl_opt_pass *make_pass_narrow_gp_writes (gcc::context *);
poly_uint64 aarch64_regmode_natural_size (machine_mode);
diff --git a/gcc/config/aarch64/aarch64.opt b/gcc/config/aarch64/aarch64.opt
index deccc6c8887..21e9500996d 100644
--- a/gcc/config/aarch64/aarch64.opt
+++ b/gcc/config/aarch64/aarch64.opt
@@ -461,6 +461,11 @@ individual writeback accesses where possible. A value of
two means we
also try to opportunistically form writeback opportunities by folding in
trailing destructive updates of the base register used by a pair.
+-param=aarch64-split-ldp-stlf=
+Target Joined UInteger Var(aarch64_split_ldp_stlf) Init(0) IntegerRange(0, 32)
Param
+Maximum number of preceding instructions to search when splitting an LDP
+to improve store-to-load forwarding. A value of zero disables the pass.
+
-param=aarch64-tag-memory-loop-threshold=
Target Joined UInteger Var(aarch64_tag_memory_loop_threshold) Init(10)
IntegerRange(0, 65536) Param
Param to control the threshold in number of granules beyond which an
diff --git a/gcc/config/aarch64/t-aarch64 b/gcc/config/aarch64/t-aarch64
index 3942a1049b9..936540aeb4e 100644
--- a/gcc/config/aarch64/t-aarch64
+++ b/gcc/config/aarch64/t-aarch64
@@ -238,6 +238,12 @@ aarch64-ldp-fusion.o:
$(srcdir)/config/aarch64/aarch64-ldp-fusion.cc \
$(COMPILER) -c $(ALL_COMPILERFLAGS) $(ALL_CPPFLAGS) $(INCLUDES) \
$(srcdir)/config/aarch64/aarch64-ldp-fusion.cc
+aarch64-ldp-split.o: $(srcdir)/config/aarch64/aarch64-ldp-split.cc \
+ $(CONFIG_H) $(SYSTEM_H) $(CORETYPES_H) $(BACKEND_H) $(RTL_H) $(DF_H) \
+ $(INSN_ATTR_H) insn-config.h $(RECOG_H) tree-pass.h
+ $(COMPILER) -c $(ALL_COMPILERFLAGS) $(ALL_CPPFLAGS) $(INCLUDES) \
+ $(srcdir)/config/aarch64/aarch64-ldp-split.cc
+
aarch64-sched-dispatch.o: $(srcdir)/config/aarch64/aarch64-sched-dispatch.cc \
$(CONFIG_H) $(SYSTEM_H) $(CORETYPES_H) $(BACKEND_H) $(RTL_H) \
$(INSN_ATTR_H) $(REGSET_H) sched-int.h $(DUMPFILE_H) \
diff --git a/gcc/doc/params.texi b/gcc/doc/params.texi
index 26b34a59aa9..1ae93ae6c75 100644
--- a/gcc/doc/params.texi
+++ b/gcc/doc/params.texi
@@ -2061,6 +2061,14 @@ accesses where possible. A value of two means we also
try to opportunistically
form writeback opportunities by folding in trailing destructive updates of the
base register used by a pair.
+@paindex aarch64-split-ldp-stlf
+@item aarch64-split-ldp-stlf
+Maximum number of preceding instructions that the AArch64 backend searches
+for a scalar store that writes one half of a later @code{ldp}. When such a
+store is found, the backend can split the @code{ldp} into two scalar loads to
+improve store-to-load forwarding. A value of zero, which is the default,
+disables the transformation.
+
@paindex aarch64-loop-vect-issue-rate-niters
@item aarch64-loop-vect-issue-rate-niters
The tuning for some AArch64 CPUs tries to take both latencies and issue
diff --git a/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-1.c
b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-1.c
new file mode 100644
index 00000000000..ca4ca4821b1
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-1.c
@@ -0,0 +1,43 @@
+/* { dg-options "-O2 --param=aarch64-split-ldp-stlf=16" } */
+
+__attribute__ ((noinline, noclone))
+long
+split_low (long *p, long value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+__attribute__ ((noinline, noclone))
+long
+split_high (long *p, long value)
+{
+ p[2] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+__attribute__ ((noinline, noclone))
+int
+split_low_si (int *p, int value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+extern void consume (long, long);
+
+__attribute__ ((noinline, noclone))
+void
+split_base_dest_overlap (long *p, long value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ consume (p[1], p[2]);
+}
+
+/* { dg-final { scan-assembler-not {\tldp\t} } } */
+/* { dg-final { scan-assembler-times {\tldr\t} 8 } } */
+/* { dg-final { scan-assembler-times {\tldr\tx1, \[x0, 16\]\n\tldr\tx0, \[x0,
8\]} 1 } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-2.c
b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-2.c
new file mode 100644
index 00000000000..2ad7e2d5825
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-2.c
@@ -0,0 +1,13 @@
+/* { dg-options "-O2" } */
+
+__attribute__ ((noinline, noclone))
+long
+disabled_by_default (long *p, long value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+/* { dg-final { scan-assembler-times {\tldp\t} 1 } } */
+/* { dg-final { scan-assembler-not {\tldr\t} } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-3.c
b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-3.c
new file mode 100644
index 00000000000..930ada6836d
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-3.c
@@ -0,0 +1,22 @@
+/* { dg-options "-O2 --param=aarch64-split-ldp-stlf=16" } */
+
+__attribute__ ((noinline, noclone))
+long
+no_overlap (long *p, long value)
+{
+ p[4] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+__attribute__ ((noinline, noclone))
+long
+wrong_store_width (long *p, int value)
+{
+ *(int *) ((char *) p + 8) = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+/* { dg-final { scan-assembler-times {\tldp\t} 2 } } */
+/* { dg-final { scan-assembler-not {\tldr\t} } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-4.c
b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-4.c
new file mode 100644
index 00000000000..3e08bcbea05
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-4.c
@@ -0,0 +1,14 @@
+/* { dg-options "-O2 --param=aarch64-split-ldp-stlf=16" } */
+/* { dg-additional-options "-fnon-call-exceptions" } */
+
+__attribute__ ((noinline, noclone))
+long
+preserve_faulting_pair (long *p, long value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+/* { dg-final { scan-assembler-times {\tldp\t} 1 } } */
+/* { dg-final { scan-assembler-not {\tldr\t} } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-5.c
b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-5.c
new file mode 100644
index 00000000000..a19551c4374
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-5.c
@@ -0,0 +1,13 @@
+/* { dg-options "-Os --param=aarch64-split-ldp-stlf=16" } */
+
+__attribute__ ((noinline, noclone))
+long
+optimize_for_size (long *p, long value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+/* { dg-final { scan-assembler-times {\tldp\t} 1 } } */
+/* { dg-final { scan-assembler-not {\tldr\t} } } */
diff --git a/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-6.c
b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-6.c
new file mode 100644
index 00000000000..bd59fe77b47
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/split-ldp-stlf-6.c
@@ -0,0 +1,13 @@
+/* { dg-options "-O2 --param=aarch64-split-ldp-stlf=1" } */
+
+__attribute__ ((noinline, noclone))
+long
+outside_search_window (long *p, long value)
+{
+ p[1] = value;
+ __asm__ __volatile__ ("" ::: "memory");
+ return p[1] + p[2];
+}
+
+/* { dg-final { scan-assembler-times {\tldp\t} 1 } } */
+/* { dg-final { scan-assembler-not {\tldr\t} } } */
--
2.43.0