[llvm] Add PhaseOrdering test mirroring missed optimization on empty std::set (PR #192840)
Jiří Filek via llvm-commits
llvm-commits at lists.llvm.org
Sun Apr 19 03:03:04 PDT 2026
https://github.com/fileho created https://github.com/llvm/llvm-project/pull/192840
Adds a PhaseOrdering test mirroring missed optimization on empty `std::set`. The `erase` in not inlined due to missing store-to-load forwarding between inlining attempts, so inliner sees it as recursive.
Part of #183994
Addressed in #190607
>From 1bbba8cdb00d53571dd96f3fc6a3fe72af9ba207 Mon Sep 17 00:00:00 2001
From: Jiri Filek <jiri.filek at gmail.com>
Date: Sun, 19 Apr 2026 11:51:32 +0200
Subject: [PATCH] Add PhaseOrdering test mirroring missing optimization on
empty std::set
---
.../PhaseOrdering/inline-store-to-load.ll | 113 ++++++++++++++++++
1 file changed, 113 insertions(+)
create mode 100644 llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
diff --git a/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll b/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
new file mode 100644
index 0000000000000..e62a4e88e8d49
--- /dev/null
+++ b/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
@@ -0,0 +1,113 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt -S -passes="default<O3>" < %s | FileCheck %s
+
+; FIXME: Mirrors missing optimization on empty std::set
+; The constructor stores null, so the destructor's erase call
+; should be simplified to a no-op, making the entire function ret void.
+
+; Test that the CGSCC inliner forwards stores to load arguments after
+; inlining. This addresses a phase ordering issue: the constructor is
+; inlined first (producing stores), and then the destructor's load
+; arguments should see the stored constants. Without this forwarding,
+; the recursive erase function appears too expensive to inline.
+;
+; This models the std::set<int>{} pattern where the constructor stores
+; null to the root pointer and the destructor checks it before recursive
+; tree deletion (_Rb_tree::_M_erase).
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+declare void @free(ptr)
+declare void @use(i64)
+
+; Models _Rb_tree constructor: stores null root + zero node count.
+define internal void @ctor(ptr %this) {
+ store ptr null, ptr %this, align 8
+ %count = getelementptr inbounds i8, ptr %this, i64 8
+ store i64 0, ptr %count, align 8
+ ret void
+}
+
+; Models ~_Rb_tree: loads root and calls the recursive eraser.
+define internal void @dtor(ptr %this) {
+ %root = load ptr, ptr %this, align 8
+ call void @erase(ptr %root)
+ ret void
+}
+
+; Models _Rb_tree::_M_erase — recursive node deletion.
+; With null: icmp + branch to done → trivially cheap.
+; With unknown ptr: recursive calls + external calls → too expensive.
+define internal void @erase(ptr %node) {
+; CHECK-LABEL: define internal fastcc void @erase(
+; CHECK-SAME: ptr captures(address_is_null) [[NODE:%.*]]) unnamed_addr {
+; CHECK-NEXT: [[IS_NULL:%.*]] = icmp eq ptr [[NODE]], null
+; CHECK-NEXT: br i1 [[IS_NULL]], label %[[COMMON_RET1:.*]], label %[[RECURSE:.*]]
+; CHECK: [[COMMON_RET1]]:
+; CHECK-NEXT: ret void
+; CHECK: [[RECURSE]]:
+; CHECK-NEXT: [[LEFT_VAL:%.*]] = load ptr, ptr [[NODE]], align 8
+; CHECK-NEXT: tail call fastcc void @erase(ptr [[LEFT_VAL]])
+; CHECK-NEXT: [[RIGHT_PTR:%.*]] = getelementptr inbounds nuw i8, ptr [[NODE]], i64 8
+; CHECK-NEXT: [[RIGHT_VAL:%.*]] = load ptr, ptr [[RIGHT_PTR]], align 8
+; CHECK-NEXT: tail call fastcc void @erase(ptr [[RIGHT_VAL]])
+; CHECK-NEXT: [[D1:%.*]] = getelementptr inbounds nuw i8, ptr [[NODE]], i64 16
+; CHECK-NEXT: [[V1:%.*]] = load i64, ptr [[D1]], align 8
+; CHECK-NEXT: tail call void @use(i64 [[V1]])
+; CHECK-NEXT: [[D2:%.*]] = getelementptr inbounds nuw i8, ptr [[NODE]], i64 24
+; CHECK-NEXT: [[V2:%.*]] = load i64, ptr [[D2]], align 8
+; CHECK-NEXT: tail call void @use(i64 [[V2]])
+; CHECK-NEXT: [[D3:%.*]] = getelementptr inbounds nuw i8, ptr [[NODE]], i64 32
+; CHECK-NEXT: [[V3:%.*]] = load i64, ptr [[D3]], align 8
+; CHECK-NEXT: tail call void @use(i64 [[V3]])
+; CHECK-NEXT: [[D4:%.*]] = getelementptr inbounds nuw i8, ptr [[NODE]], i64 40
+; CHECK-NEXT: [[V4:%.*]] = load i64, ptr [[D4]], align 8
+; CHECK-NEXT: tail call void @use(i64 [[V4]])
+; CHECK-NEXT: [[D5:%.*]] = getelementptr inbounds nuw i8, ptr [[NODE]], i64 48
+; CHECK-NEXT: [[V5:%.*]] = load i64, ptr [[D5]], align 8
+; CHECK-NEXT: tail call void @use(i64 [[V5]])
+; CHECK-NEXT: tail call void @free(ptr nonnull [[NODE]])
+; CHECK-NEXT: br label %[[COMMON_RET1]]
+;
+ %is_null = icmp eq ptr %node, null
+ br i1 %is_null, label %done, label %recurse
+
+recurse:
+ %left_val = load ptr, ptr %node, align 8
+ call void @erase(ptr %left_val)
+ %right_ptr = getelementptr inbounds i8, ptr %node, i64 8
+ %right_val = load ptr, ptr %right_ptr, align 8
+ call void @erase(ptr %right_val)
+ %d1 = getelementptr inbounds i8, ptr %node, i64 16
+ %v1 = load i64, ptr %d1, align 8
+ call void @use(i64 %v1)
+ %d2 = getelementptr inbounds i8, ptr %node, i64 24
+ %v2 = load i64, ptr %d2, align 8
+ call void @use(i64 %v2)
+ %d3 = getelementptr inbounds i8, ptr %node, i64 32
+ %v3 = load i64, ptr %d3, align 8
+ call void @use(i64 %v3)
+ %d4 = getelementptr inbounds i8, ptr %node, i64 40
+ %v4 = load i64, ptr %d4, align 8
+ call void @use(i64 %v4)
+ %d5 = getelementptr inbounds i8, ptr %node, i64 48
+ %v5 = load i64, ptr %d5, align 8
+ call void @use(i64 %v5)
+ call void @free(ptr %node)
+ br label %done
+
+done:
+ ret void
+}
+
+define void @test_empty_tree() {
+; CHECK-LABEL: define void @test_empty_tree() local_unnamed_addr {
+; CHECK-NEXT: tail call fastcc void @erase(ptr null)
+; CHECK-NEXT: ret void
+;
+ %tree = alloca ptr, align 8
+ call void @ctor(ptr %tree)
+ call void @dtor(ptr %tree)
+ ret void
+}
More information about the llvm-commits
mailing list