[llvm] [InlineCost] Use store-to-load forwarding to resolve call arguments (PR #190607)
Jiří Filek via llvm-commits
llvm-commits at lists.llvm.org
Sun Apr 19 03:19:26 PDT 2026
https://github.com/fileho updated https://github.com/llvm/llvm-project/pull/190607
>From 41169e693445fbbd246f3a394a08eed8efc25292 Mon Sep 17 00:00:00 2001
From: Jiri Filek <jiri.filek at gmail.com>
Date: Mon, 6 Apr 2026 14:35:59 +0200
Subject: [PATCH 1/4] [InlineCost] Use store-to-load forwarding to resolve call
arguments
Use FindAvailableLoadedValue to resolve load instructions in call arguments to constants before inline cost analysis. This gives inliner more precise cost estimate
---
llvm/lib/Analysis/InlineCost.cpp | 24 +++++-
.../Transforms/Inline/inline_store_to_load.ll | 73 +++++++++++++++++++
2 files changed, 94 insertions(+), 3 deletions(-)
create mode 100644 llvm/test/Transforms/Inline/inline_store_to_load.ll
diff --git a/llvm/lib/Analysis/InlineCost.cpp b/llvm/lib/Analysis/InlineCost.cpp
index 06ff9cc80f638..34d781ae29338 100644
--- a/llvm/lib/Analysis/InlineCost.cpp
+++ b/llvm/lib/Analysis/InlineCost.cpp
@@ -23,6 +23,7 @@
#include "llvm/Analysis/DomConditionCache.h"
#include "llvm/Analysis/EphemeralValuesCache.h"
#include "llvm/Analysis/InstructionSimplify.h"
+#include "llvm/Analysis/Loads.h"
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/MemoryBuiltins.h"
#include "llvm/Analysis/OptimizationRemarkEmitter.h"
@@ -2931,11 +2932,28 @@ InlineResult CallAnalyzer::analyze() {
auto CAI = CandidateCall.arg_begin();
for (Argument &FAI : F.args()) {
assert(CAI != CandidateCall.arg_end());
- SimplifiedValues[&FAI] = *CAI;
- if (isa<Constant>(*CAI))
+ Value *CallerArg = *CAI;
+
+ // Simple store-to-load forwarding of arguments in the caller.
+ // This can greatly reduce the function cost via early returns.
+ if (auto *LI = dyn_cast<LoadInst>(CallerArg)) {
+ BasicBlock::iterator ScanFrom = LI->getIterator();
+ if (Value *Loaded =
+ FindAvailableLoadedValue(LI, LI->getParent(), ScanFrom, 0)) {
+ if (auto *C = dyn_cast<Constant>(Loaded)) {
+ if (C->getType() == LI->getType())
+ CallerArg = C;
+ else if (C->isNullValue())
+ CallerArg = Constant::getNullValue(LI->getType());
+ }
+ }
+ }
+
+ SimplifiedValues[&FAI] = CallerArg;
+ if (isa<Constant>(CallerArg))
++NumConstantArgs;
- Value *PtrArg = *CAI;
+ Value *PtrArg = CallerArg;
if (ConstantInt *C = stripAndComputeInBoundsConstantOffsets(PtrArg)) {
ConstantOffsetPtrs[&FAI] = std::make_pair(PtrArg, C->getValue());
diff --git a/llvm/test/Transforms/Inline/inline_store_to_load.ll b/llvm/test/Transforms/Inline/inline_store_to_load.ll
new file mode 100644
index 0000000000000..919dc9aafa403
--- /dev/null
+++ b/llvm/test/Transforms/Inline/inline_store_to_load.ll
@@ -0,0 +1,73 @@
+; RUN: opt < %s -passes=inline -inline-threshold=20 -S | FileCheck %s
+
+; Test that the inliner can use store-to-load forwarding to resolve call
+; arguments to constants. We use -inline-threshold=20 so that @callee is
+; only inlined when the constant argument enables dead-branch elimination.
+
+target datalayout = "p:64:64"
+
+; Two paths: mode==0 is trivial (ret), otherwise too expensive to inline.
+define i32 @callee(i32 %mode, ptr %p) {
+entry:
+ %cmp = icmp eq i32 %mode, 0
+ br i1 %cmp, label %fast, label %slow
+fast:
+ %v = load i32, ptr %p
+ ret i32 %v
+slow:
+ %a1 = load volatile i32, ptr %p
+ %a2 = load volatile i32, ptr %p
+ %x1 = add i32 %a1, %a2
+ %a3 = load volatile i32, ptr %p
+ %x2 = add i32 %x1, %a3
+ %a4 = load volatile i32, ptr %p
+ %x3 = add i32 %x2, %a4
+ %a5 = load volatile i32, ptr %p
+ %x4 = add i32 %x3, %a5
+ ret i32 %x4
+}
+
+; Trivial when called with null, otherwise too expensive to inline.
+define void @recursive_callee(ptr %x) {
+entry:
+ %cmp = icmp eq ptr %x, null
+ br i1 %cmp, label %done, label %recurse
+recurse:
+ %next = load ptr, ptr %x
+ call void @recursive_callee(ptr %next)
+ %v = load volatile i32, ptr %x
+ br label %done
+done:
+ ret void
+}
+
+; Store-to-load forwarding resolves %mode to 0, making only the fast path
+; reachable and the callee cheap enough to inline.
+; CHECK-LABEL: define i32 @caller_store_forward(
+; CHECK: %cmp.i = icmp eq i32 %mode, 0
+; CHECK: callee.exit:
+; CHECK-NEXT: %{{.*}} = phi i32
+; CHECK-NEXT: ret i32
+define i32 @caller_store_forward() {
+entry:
+ %p = alloca i32
+ store i32 0, ptr %p
+ %mode = load i32, ptr %p
+ %r = call i32 @callee(i32 %mode, ptr %p)
+ ret i32 %r
+}
+
+; Memset-to-load forwarding converts the zero-filled integer to a null
+; pointer, making the null check take the early exit.
+; CHECK-LABEL: define void @caller_memset_ptr(
+; CHECK: %cmp.i = icmp eq ptr %x, null
+; CHECK: recursive_callee.exit:
+; CHECK-NEXT: ret void
+define void @caller_memset_ptr() {
+entry:
+ %p = alloca ptr
+ call void @llvm.memset.p0.i64(ptr %p, i8 0, i64 8, i1 false)
+ %x = load ptr, ptr %p
+ call void @recursive_callee(ptr %x)
+ ret void
+}
>From a9eb1692ec01e4ddd3a4eb1d0794f3a8bfecc610 Mon Sep 17 00:00:00 2001
From: Jiri Filek <jiri.filek at gmail.com>
Date: Mon, 13 Apr 2026 20:37:51 +0200
Subject: [PATCH 2/4] revert changes in InlineCost
---
llvm/lib/Analysis/InlineCost.cpp | 24 +++---------------------
1 file changed, 3 insertions(+), 21 deletions(-)
diff --git a/llvm/lib/Analysis/InlineCost.cpp b/llvm/lib/Analysis/InlineCost.cpp
index 34d781ae29338..06ff9cc80f638 100644
--- a/llvm/lib/Analysis/InlineCost.cpp
+++ b/llvm/lib/Analysis/InlineCost.cpp
@@ -23,7 +23,6 @@
#include "llvm/Analysis/DomConditionCache.h"
#include "llvm/Analysis/EphemeralValuesCache.h"
#include "llvm/Analysis/InstructionSimplify.h"
-#include "llvm/Analysis/Loads.h"
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/MemoryBuiltins.h"
#include "llvm/Analysis/OptimizationRemarkEmitter.h"
@@ -2932,28 +2931,11 @@ InlineResult CallAnalyzer::analyze() {
auto CAI = CandidateCall.arg_begin();
for (Argument &FAI : F.args()) {
assert(CAI != CandidateCall.arg_end());
- Value *CallerArg = *CAI;
-
- // Simple store-to-load forwarding of arguments in the caller.
- // This can greatly reduce the function cost via early returns.
- if (auto *LI = dyn_cast<LoadInst>(CallerArg)) {
- BasicBlock::iterator ScanFrom = LI->getIterator();
- if (Value *Loaded =
- FindAvailableLoadedValue(LI, LI->getParent(), ScanFrom, 0)) {
- if (auto *C = dyn_cast<Constant>(Loaded)) {
- if (C->getType() == LI->getType())
- CallerArg = C;
- else if (C->isNullValue())
- CallerArg = Constant::getNullValue(LI->getType());
- }
- }
- }
-
- SimplifiedValues[&FAI] = CallerArg;
- if (isa<Constant>(CallerArg))
+ SimplifiedValues[&FAI] = *CAI;
+ if (isa<Constant>(*CAI))
++NumConstantArgs;
- Value *PtrArg = CallerArg;
+ Value *PtrArg = *CAI;
if (ConstantInt *C = stripAndComputeInBoundsConstantOffsets(PtrArg)) {
ConstantOffsetPtrs[&FAI] = std::make_pair(PtrArg, C->getValue());
>From 5f97c3474be44c92d640c92b8806e44b735f6f94 Mon Sep 17 00:00:00 2001
From: Jiri Filek <jiri.filek at gmail.com>
Date: Mon, 13 Apr 2026 20:42:24 +0200
Subject: [PATCH 3/4] [Inliner] Add store-to-load forwarding to inliner -
forward only when some inlining already happended - add phase ordering test
---
llvm/lib/Transforms/IPO/Inliner.cpp | 37 +++++++++
.../Transforms/Inline/inline_store_to_load.ll | 49 +++++++-----
.../PhaseOrdering/inline-store-to-load.ll | 77 +++++++++++++++++++
3 files changed, 145 insertions(+), 18 deletions(-)
create mode 100644 llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
diff --git a/llvm/lib/Transforms/IPO/Inliner.cpp b/llvm/lib/Transforms/IPO/Inliner.cpp
index fb376562f6781..9c5c5788b69a6 100644
--- a/llvm/lib/Transforms/IPO/Inliner.cpp
+++ b/llvm/lib/Transforms/IPO/Inliner.cpp
@@ -30,6 +30,7 @@
#include "llvm/Analysis/InlineAdvisor.h"
#include "llvm/Analysis/InlineCost.h"
#include "llvm/Analysis/LazyCallGraph.h"
+#include "llvm/Analysis/Loads.h"
#include "llvm/Analysis/OptimizationRemarkEmitter.h"
#include "llvm/Analysis/ProfileSummaryInfo.h"
#include "llvm/Analysis/ReplayInlineAdvisor.h"
@@ -80,6 +81,14 @@ static cl::opt<int> IntraSCCCostMultiplier(
"multiplied by intra-scc-cost-multiplier). This is to prevent tons of "
"inlining through a child SCC which can cause terrible compile times"));
+static cl::opt<unsigned> InlinerForwardingScanLimit(
+ "inliner-forwarding-scan-limit", cl::init(16), cl::Hidden,
+ cl::desc("Maximum number of instructions to scan backward for "
+ "store-to-load forwarding in subsequent inlining decisions. "
+ "DefMaxInstsToScan=6 is not enough and misses inlining "
+ "opportunities (e.g. when class stores into mutiple members in "
+ "ctor and afterwards calls a function reading those members)"));
+
/// A flag for test, so we can print the content of the advisor when running it
/// as part of the default (e.g. -O3) pipeline.
static cl::opt<bool> KeepAdvisorForPrinting("keep-inline-advisor-for-printing",
@@ -358,6 +367,34 @@ PreservedAnalyses InlinerPass::run(LazyCallGraph::SCC &InitialC,
continue;
}
+ // Store-to-load forwarding, loads can be sometimes simplified to
+ // constants from stores introduced by previous inlining
+ if (DidInline) {
+ for (Value *Arg : CB->args()) {
+ auto *LI = dyn_cast<LoadInst>(Arg);
+ if (!LI || !LI->isSimple())
+ continue;
+ BasicBlock::iterator BBI = LI->getIterator();
+ Value *Available = FindAvailableLoadedValue(
+ LI, LI->getParent(), BBI, InlinerForwardingScanLimit);
+ if (!Available)
+ continue;
+ auto *C = dyn_cast<Constant>(Available);
+ if (!C)
+ continue;
+ // Handle type mismatches from memset forwarding (e.g. memset
+ // writes i64 0 but the load type is ptr).
+ if (C->getType() != LI->getType()) {
+ if (C->isNullValue())
+ C = Constant::getNullValue(LI->getType());
+ else
+ continue;
+ }
+ LI->replaceAllUsesWith(C);
+ LI->eraseFromParent();
+ }
+ }
+
std::unique_ptr<InlineAdvice> Advice =
Advisor.getAdvice(*CB, OnlyMandatory);
diff --git a/llvm/test/Transforms/Inline/inline_store_to_load.ll b/llvm/test/Transforms/Inline/inline_store_to_load.ll
index 919dc9aafa403..151e138cee789 100644
--- a/llvm/test/Transforms/Inline/inline_store_to_load.ll
+++ b/llvm/test/Transforms/Inline/inline_store_to_load.ll
@@ -1,8 +1,10 @@
-; RUN: opt < %s -passes=inline -inline-threshold=20 -S | FileCheck %s
+; RUN: opt -S -passes="cgscc(inline)" < %s | FileCheck %s
-; Test that the inliner can use store-to-load forwarding to resolve call
-; arguments to constants. We use -inline-threshold=20 so that @callee is
-; only inlined when the constant argument enables dead-branch elimination.
+; Test that the CGSCC inliner performs store-to-load forwarding for call
+; arguments after inlining a function in the same caller. The first call
+; (@init or @init_memset) is inlined, producing stores. Forwarding then
+; resolves the subsequent load to a constant, enabling inlining of the
+; second callee.
target datalayout = "p:64:64"
@@ -41,17 +43,29 @@ done:
ret void
}
-; Store-to-load forwarding resolves %mode to 0, making only the fast path
-; reachable and the callee cheap enough to inline.
-; CHECK-LABEL: define i32 @caller_store_forward(
-; CHECK: %cmp.i = icmp eq i32 %mode, 0
-; CHECK: callee.exit:
-; CHECK-NEXT: %{{.*}} = phi i32
-; CHECK-NEXT: ret i32
+; Trivially cheap — inlined first, producing a store.
+define internal void @init_i32(ptr %p) {
+ store i32 0, ptr %p
+ ret void
+}
+
+; Trivially cheap — inlined first, producing a memset.
+define internal void @init_memset(ptr %p) {
+ call void @llvm.memset.p0.i64(ptr %p, i8 0, i64 8, i1 false)
+ ret void
+}
+
+declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
+
+; After inlining @init_i32: store 0 → %p.
+; Forwarding resolves %mode to 0, making only the fast path reachable.
+; CHECK-LABEL: @caller_store_forward(
+; CHECK-NOT: call i32 @callee
+; CHECK: ret i32
define i32 @caller_store_forward() {
entry:
%p = alloca i32
- store i32 0, ptr %p
+ call void @init_i32(ptr %p)
%mode = load i32, ptr %p
%r = call i32 @callee(i32 %mode, ptr %p)
ret i32 %r
@@ -59,14 +73,13 @@ entry:
; Memset-to-load forwarding converts the zero-filled integer to a null
; pointer, making the null check take the early exit.
-; CHECK-LABEL: define void @caller_memset_ptr(
-; CHECK: %cmp.i = icmp eq ptr %x, null
-; CHECK: recursive_callee.exit:
-; CHECK-NEXT: ret void
-define void @caller_memset_ptr() {
+; CHECK-LABEL: @caller_memset_forward(
+; CHECK-NOT: call void @recursive_callee
+; CHECK: ret void
+define void @caller_memset_forward() {
entry:
%p = alloca ptr
- call void @llvm.memset.p0.i64(ptr %p, i8 0, i64 8, i1 false)
+ call void @init_memset(ptr %p)
%x = load ptr, ptr %p
call void @recursive_callee(ptr %x)
ret void
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..5f8d4755c357a
--- /dev/null
+++ b/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
@@ -0,0 +1,77 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt -S -passes="default<O3>" < %s | FileCheck %s
+
+; 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) {
+ %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
+}
+
+; CHECK-LABEL: @test_empty_tree(
+; CHECK-NEXT: ret void
+define void @test_empty_tree() {
+ %tree = alloca ptr, align 8
+ call void @ctor(ptr %tree)
+ call void @dtor(ptr %tree)
+ ret void
+}
>From 9398928068f3dd433d526062c1572df7e5edcfe8 Mon Sep 17 00:00:00 2001
From: Jiri Filek <jiri.filek at gmail.com>
Date: Sun, 19 Apr 2026 12:19:06 +0200
Subject: [PATCH 4/4] Patch tests by update_test_checks.py
---
.../Transforms/Inline/inline_store_to_load.ll | 37 +++++++++++++++----
.../PhaseOrdering/inline-store-to-load.ll | 6 ++-
2 files changed, 33 insertions(+), 10 deletions(-)
diff --git a/llvm/test/Transforms/Inline/inline_store_to_load.ll b/llvm/test/Transforms/Inline/inline_store_to_load.ll
index 151e138cee789..ca8d8bc0930e0 100644
--- a/llvm/test/Transforms/Inline/inline_store_to_load.ll
+++ b/llvm/test/Transforms/Inline/inline_store_to_load.ll
@@ -1,3 +1,4 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -S -passes="cgscc(inline)" < %s | FileCheck %s
; Test that the CGSCC inliner performs store-to-load forwarding for call
@@ -9,7 +10,7 @@
target datalayout = "p:64:64"
; Two paths: mode==0 is trivial (ret), otherwise too expensive to inline.
-define i32 @callee(i32 %mode, ptr %p) {
+define internal i32 @callee(i32 %mode, ptr %p) {
entry:
%cmp = icmp eq i32 %mode, 0
br i1 %cmp, label %fast, label %slow
@@ -30,7 +31,20 @@ slow:
}
; Trivial when called with null, otherwise too expensive to inline.
-define void @recursive_callee(ptr %x) {
+define internal void @recursive_callee(ptr %x) {
+; CHECK-LABEL: define internal void @recursive_callee(
+; CHECK-SAME: ptr [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq ptr [[X]], null
+; CHECK-NEXT: br i1 [[CMP]], label %[[DONE:.*]], label %[[RECURSE:.*]]
+; CHECK: [[RECURSE]]:
+; CHECK-NEXT: [[NEXT:%.*]] = load ptr, ptr [[X]], align 8
+; CHECK-NEXT: call void @recursive_callee(ptr [[NEXT]])
+; CHECK-NEXT: [[V:%.*]] = load volatile i32, ptr [[X]], align 4
+; CHECK-NEXT: br label %[[DONE]]
+; CHECK: [[DONE]]:
+; CHECK-NEXT: ret void
+;
entry:
%cmp = icmp eq ptr %x, null
br i1 %cmp, label %done, label %recurse
@@ -59,10 +73,14 @@ declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
; After inlining @init_i32: store 0 → %p.
; Forwarding resolves %mode to 0, making only the fast path reachable.
-; CHECK-LABEL: @caller_store_forward(
-; CHECK-NOT: call i32 @callee
-; CHECK: ret i32
define i32 @caller_store_forward() {
+; CHECK-LABEL: define i32 @caller_store_forward() {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[P:%.*]] = alloca i32, align 4
+; CHECK-NEXT: store i32 0, ptr [[P]], align 4
+; CHECK-NEXT: [[V_I:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: ret i32 [[V_I]]
+;
entry:
%p = alloca i32
call void @init_i32(ptr %p)
@@ -73,10 +91,13 @@ entry:
; Memset-to-load forwarding converts the zero-filled integer to a null
; pointer, making the null check take the early exit.
-; CHECK-LABEL: @caller_memset_forward(
-; CHECK-NOT: call void @recursive_callee
-; CHECK: ret void
define void @caller_memset_forward() {
+; CHECK-LABEL: define void @caller_memset_forward() {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[P:%.*]] = alloca ptr, align 8
+; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[P]], i8 0, i64 8, i1 false)
+; CHECK-NEXT: ret void
+;
entry:
%p = alloca ptr
call void @init_memset(ptr %p)
diff --git a/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll b/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
index 5f8d4755c357a..59bc38a7a2c24 100644
--- a/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
+++ b/llvm/test/Transforms/PhaseOrdering/inline-store-to-load.ll
@@ -67,9 +67,11 @@ done:
ret void
}
-; CHECK-LABEL: @test_empty_tree(
-; CHECK-NEXT: ret void
define void @test_empty_tree() {
+; CHECK-LABEL: define void @test_empty_tree(
+; CHECK-SAME: ) local_unnamed_addr #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT: ret void
+;
%tree = alloca ptr, align 8
call void @ctor(ptr %tree)
call void @dtor(ptr %tree)
More information about the llvm-commits
mailing list