[llvm] Add PhaseOrdering test mirroring missed optimization on empty std::set (PR #192840)

via llvm-commits llvm-commits at lists.llvm.org
Sun Apr 19 03:03:37 PDT 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Jiří Filek (fileho)

<details>
<summary>Changes</summary>

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

---
Full diff: https://github.com/llvm/llvm-project/pull/192840.diff


1 Files Affected:

- (added) llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll (+113) 


``````````diff
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
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/192840


More information about the llvm-commits mailing list