-
Notifications
You must be signed in to change notification settings - Fork 14.7k
release/21.x [ObjCARC] Delete empty autoreleasepools with no autoreleases in them and remove ObjCARCAPElimPass #150771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@llvm/pr-subscribers-clang @llvm/pr-subscribers-llvm-transforms Author: AZero13 (AZero13) ChangesAs per the new guidelines, I finished this with the intention of being admitted into the 21.x release. It was finished for a few weeks before it was merged, and I would like to backport this. I apolgoize if this is getting too close to the merge window, but this is just in time, no? Patch is 32.47 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/150771.diff 11 Files Affected:
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 2f6d4c414e737..17d83ff5dbebe 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -1027,12 +1027,6 @@ void EmitAssemblyHelper::RunOptimizationPipeline(
MPM.addPass(
createModuleToFunctionPassAdaptor(ObjCARCExpandPass()));
});
- PB.registerPipelineEarlySimplificationEPCallback(
- [](ModulePassManager &MPM, OptimizationLevel Level,
- ThinOrFullLTOPhase) {
- if (Level != OptimizationLevel::O0)
- MPM.addPass(ObjCARCAPElimPass());
- });
PB.registerScalarOptimizerLateEPCallback(
[](FunctionPassManager &FPM, OptimizationLevel Level) {
if (Level != OptimizationLevel::O0)
diff --git a/llvm/include/llvm/Transforms/ObjCARC.h b/llvm/include/llvm/Transforms/ObjCARC.h
index c927513469a35..c4b4c4f0b09c6 100644
--- a/llvm/include/llvm/Transforms/ObjCARC.h
+++ b/llvm/include/llvm/Transforms/ObjCARC.h
@@ -35,10 +35,6 @@ struct ObjCARCContractPass : public PassInfoMixin<ObjCARCContractPass> {
LLVM_ABI PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};
-struct ObjCARCAPElimPass : public PassInfoMixin<ObjCARCAPElimPass> {
- LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
-};
-
struct ObjCARCExpandPass : public PassInfoMixin<ObjCARCExpandPass> {
LLVM_ABI PreservedAnalyses run(Function &M, FunctionAnalysisManager &AM);
};
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 9a943155aa19f..8d86e2b996449 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -119,7 +119,6 @@ MODULE_PASS("module-inline", ModuleInlinerPass())
MODULE_PASS("name-anon-globals", NameAnonGlobalPass())
MODULE_PASS("no-op-module", NoOpModulePass())
MODULE_PASS("nsan", NumericalStabilitySanitizerPass())
-MODULE_PASS("objc-arc-apelim", ObjCARCAPElimPass())
MODULE_PASS("openmp-opt", OpenMPOptPass())
MODULE_PASS("openmp-opt-postlink",
OpenMPOptPass(ThinOrFullLTOPhase::FullLTOPostLink))
diff --git a/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h b/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
index 3fa844eda21cf..6135c7b938a3e 100644
--- a/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
+++ b/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
@@ -46,6 +46,8 @@ enum class ARCRuntimeEntryPointKind {
UnsafeClaimRV,
RetainAutorelease,
RetainAutoreleaseRV,
+ AutoreleasePoolPush,
+ AutoreleasePoolPop,
};
/// Declarations for ObjC runtime functions and constants. These are initialized
@@ -67,6 +69,8 @@ class ARCRuntimeEntryPoints {
UnsafeClaimRV = nullptr;
RetainAutorelease = nullptr;
RetainAutoreleaseRV = nullptr;
+ AutoreleasePoolPush = nullptr;
+ AutoreleasePoolPop = nullptr;
}
Function *get(ARCRuntimeEntryPointKind kind) {
@@ -101,6 +105,12 @@ class ARCRuntimeEntryPoints {
case ARCRuntimeEntryPointKind::RetainAutoreleaseRV:
return getIntrinsicEntryPoint(RetainAutoreleaseRV,
Intrinsic::objc_retainAutoreleaseReturnValue);
+ case ARCRuntimeEntryPointKind::AutoreleasePoolPush:
+ return getIntrinsicEntryPoint(AutoreleasePoolPush,
+ Intrinsic::objc_autoreleasePoolPush);
+ case ARCRuntimeEntryPointKind::AutoreleasePoolPop:
+ return getIntrinsicEntryPoint(AutoreleasePoolPop,
+ Intrinsic::objc_autoreleasePoolPop);
}
llvm_unreachable("Switch should be a covered switch.");
@@ -143,6 +153,12 @@ class ARCRuntimeEntryPoints {
/// Declaration for objc_retainAutoreleaseReturnValue().
Function *RetainAutoreleaseRV = nullptr;
+ /// Declaration for objc_autoreleasePoolPush().
+ Function *AutoreleasePoolPush = nullptr;
+
+ /// Declaration for objc_autoreleasePoolPop().
+ Function *AutoreleasePoolPop = nullptr;
+
Function *getIntrinsicEntryPoint(Function *&Decl, Intrinsic::ID IntID) {
if (Decl)
return Decl;
diff --git a/llvm/lib/Transforms/ObjCARC/CMakeLists.txt b/llvm/lib/Transforms/ObjCARC/CMakeLists.txt
index 80867dbc270d7..4274667a2c2b7 100644
--- a/llvm/lib/Transforms/ObjCARC/CMakeLists.txt
+++ b/llvm/lib/Transforms/ObjCARC/CMakeLists.txt
@@ -2,7 +2,6 @@ add_llvm_component_library(LLVMObjCARCOpts
ObjCARC.cpp
ObjCARCOpts.cpp
ObjCARCExpand.cpp
- ObjCARCAPElim.cpp
ObjCARCContract.cpp
DependencyAnalysis.cpp
ProvenanceAnalysis.cpp
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCAPElim.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCAPElim.cpp
deleted file mode 100644
index dceb2ebb1863e..0000000000000
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCAPElim.cpp
+++ /dev/null
@@ -1,156 +0,0 @@
-//===- ObjCARCAPElim.cpp - ObjC ARC Optimization --------------------------===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-/// \file
-///
-/// This file defines ObjC ARC optimizations. ARC stands for Automatic
-/// Reference Counting and is a system for managing reference counts for objects
-/// in Objective C.
-///
-/// This specific file implements optimizations which remove extraneous
-/// autorelease pools.
-///
-/// WARNING: This file knows about certain library functions. It recognizes them
-/// by name, and hardwires knowledge of their semantics.
-///
-/// WARNING: This file knows about how certain Objective-C library functions are
-/// used. Naive LLVM IR transformations which would otherwise be
-/// behavior-preserving may break these assumptions.
-///
-//===----------------------------------------------------------------------===//
-
-#include "llvm/ADT/STLExtras.h"
-#include "llvm/Analysis/ObjCARCAnalysisUtils.h"
-#include "llvm/Analysis/ObjCARCInstKind.h"
-#include "llvm/IR/Constants.h"
-#include "llvm/IR/InstrTypes.h"
-#include "llvm/IR/PassManager.h"
-#include "llvm/Support/Debug.h"
-#include "llvm/Support/raw_ostream.h"
-#include "llvm/Transforms/ObjCARC.h"
-
-using namespace llvm;
-using namespace llvm::objcarc;
-
-#define DEBUG_TYPE "objc-arc-ap-elim"
-
-namespace {
-
-/// Interprocedurally determine if calls made by the given call site can
-/// possibly produce autoreleases.
-bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) {
- if (const Function *Callee = CB.getCalledFunction()) {
- if (!Callee->hasExactDefinition())
- return true;
- for (const BasicBlock &BB : *Callee) {
- for (const Instruction &I : BB)
- if (const CallBase *JCB = dyn_cast<CallBase>(&I))
- // This recursion depth limit is arbitrary. It's just great
- // enough to cover known interesting testcases.
- if (Depth < 3 && !JCB->onlyReadsMemory() &&
- MayAutorelease(*JCB, Depth + 1))
- return true;
- }
- return false;
- }
-
- return true;
-}
-
-bool OptimizeBB(BasicBlock *BB) {
- bool Changed = false;
-
- Instruction *Push = nullptr;
- for (Instruction &Inst : llvm::make_early_inc_range(*BB)) {
- switch (GetBasicARCInstKind(&Inst)) {
- case ARCInstKind::AutoreleasepoolPush:
- Push = &Inst;
- break;
- case ARCInstKind::AutoreleasepoolPop:
- // If this pop matches a push and nothing in between can autorelease,
- // zap the pair.
- if (Push && cast<CallInst>(&Inst)->getArgOperand(0) == Push) {
- Changed = true;
- LLVM_DEBUG(dbgs() << "ObjCARCAPElim::OptimizeBB: Zapping push pop "
- "autorelease pair:\n"
- " Pop: "
- << Inst << "\n"
- << " Push: " << *Push
- << "\n");
- Inst.eraseFromParent();
- Push->eraseFromParent();
- }
- Push = nullptr;
- break;
- case ARCInstKind::CallOrUser:
- if (MayAutorelease(cast<CallBase>(Inst)))
- Push = nullptr;
- break;
- default:
- break;
- }
- }
-
- return Changed;
-}
-
-bool runImpl(Module &M) {
- if (!EnableARCOpts)
- return false;
-
- // If nothing in the Module uses ARC, don't do anything.
- if (!ModuleHasARC(M))
- return false;
- // Find the llvm.global_ctors variable, as the first step in
- // identifying the global constructors. In theory, unnecessary autorelease
- // pools could occur anywhere, but in practice it's pretty rare. Global
- // ctors are a place where autorelease pools get inserted automatically,
- // so it's pretty common for them to be unnecessary, and it's pretty
- // profitable to eliminate them.
- GlobalVariable *GV = M.getGlobalVariable("llvm.global_ctors");
- if (!GV)
- return false;
-
- assert(GV->hasDefinitiveInitializer() &&
- "llvm.global_ctors is uncooperative!");
-
- bool Changed = false;
-
- // Dig the constructor functions out of GV's initializer.
- ConstantArray *Init = cast<ConstantArray>(GV->getInitializer());
- for (User::op_iterator OI = Init->op_begin(), OE = Init->op_end();
- OI != OE; ++OI) {
- Value *Op = *OI;
- // llvm.global_ctors is an array of three-field structs where the second
- // members are constructor functions.
- Function *F = dyn_cast<Function>(cast<ConstantStruct>(Op)->getOperand(1));
- // If the user used a constructor function with the wrong signature and
- // it got bitcasted or whatever, look the other way.
- if (!F)
- continue;
- // Only look at function definitions.
- if (F->isDeclaration())
- continue;
- // Only look at functions with one basic block.
- if (std::next(F->begin()) != F->end())
- continue;
- // Ok, a single-block constructor function definition. Try to optimize it.
- Changed |= OptimizeBB(&F->front());
- }
-
- return Changed;
-}
-
-} // namespace
-
-PreservedAnalyses ObjCARCAPElimPass::run(Module &M, ModuleAnalysisManager &AM) {
- if (!runImpl(M))
- return PreservedAnalyses::all();
- PreservedAnalyses PA;
- PA.preserveSet<CFGAnalyses>();
- return PA;
-}
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index 5eb3f51d38945..66a2c7632aadc 100644
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
+++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
@@ -39,6 +39,7 @@
#include "llvm/Analysis/ObjCARCAnalysisUtils.h"
#include "llvm/Analysis/ObjCARCInstKind.h"
#include "llvm/Analysis/ObjCARCUtil.h"
+#include "llvm/Analysis/OptimizationRemarkEmitter.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/CFG.h"
#include "llvm/IR/Constant.h"
@@ -132,11 +133,8 @@ static const Value *FindSingleUseIdentifiedObject(const Value *Arg) {
//
// The second retain and autorelease can be deleted.
-// TODO: It should be possible to delete
-// objc_autoreleasePoolPush and objc_autoreleasePoolPop
-// pairs if nothing is actually autoreleased between them. Also, autorelease
-// calls followed by objc_autoreleasePoolPop calls (perhaps in ObjC++ code
-// after inlining) can be turned into plain release calls.
+// TODO: Autorelease calls followed by objc_autoreleasePoolPop calls (perhaps in
+// ObjC++ code after inlining) can be turned into plain release calls.
// TODO: Critical-edge splitting. If the optimial insertion point is
// a critical edge, the current algorithm has to fail, because it doesn't
@@ -566,6 +564,8 @@ class ObjCARCOpt {
void OptimizeReturns(Function &F);
+ void OptimizeAutoreleasePools(Function &F);
+
template <typename PredicateT>
static void cloneOpBundlesIf(CallBase *CI,
SmallVectorImpl<OperandBundleDef> &OpBundles,
@@ -2473,6 +2473,11 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) {
(1 << unsigned(ARCInstKind::AutoreleaseRV))))
OptimizeReturns(F);
+ // Optimizations for autorelease pools.
+ if (UsedInThisFunction & ((1 << unsigned(ARCInstKind::AutoreleasepoolPush)) |
+ (1 << unsigned(ARCInstKind::AutoreleasepoolPop))))
+ OptimizeAutoreleasePools(F);
+
// Gather statistics after optimization.
#ifndef NDEBUG
if (AreStatisticsEnabled()) {
@@ -2485,6 +2490,183 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) {
return Changed;
}
+/// Interprocedurally determine if calls made by the given call site can
+/// possibly produce autoreleases.
+bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) {
+ if (CB.onlyReadsMemory())
+ return false;
+
+ // This recursion depth limit is arbitrary. It's just great
+ // enough to cover known interesting testcases.
+ if (Depth > 5)
+ return true;
+
+ if (const Function *Callee = CB.getCalledFunction()) {
+ if (!Callee->hasExactDefinition())
+ return true;
+ for (const BasicBlock &BB : *Callee) {
+ for (const Instruction &I : BB) {
+ // TODO: Ignore all instructions between autorelease pools
+ ARCInstKind InstKind = GetBasicARCInstKind(&I);
+ switch (InstKind) {
+ case ARCInstKind::Autorelease:
+ case ARCInstKind::AutoreleaseRV:
+ case ARCInstKind::FusedRetainAutorelease:
+ case ARCInstKind::FusedRetainAutoreleaseRV:
+ case ARCInstKind::LoadWeak:
+ // These may produce autoreleases
+ return true;
+
+ case ARCInstKind::Retain:
+ case ARCInstKind::RetainRV:
+ case ARCInstKind::UnsafeClaimRV:
+ case ARCInstKind::RetainBlock:
+ case ARCInstKind::Release:
+ case ARCInstKind::NoopCast:
+ case ARCInstKind::LoadWeakRetained:
+ case ARCInstKind::StoreWeak:
+ case ARCInstKind::InitWeak:
+ case ARCInstKind::MoveWeak:
+ case ARCInstKind::CopyWeak:
+ case ARCInstKind::DestroyWeak:
+ case ARCInstKind::StoreStrong:
+ case ARCInstKind::AutoreleasepoolPush:
+ case ARCInstKind::AutoreleasepoolPop:
+ // These ObjC runtime functions don't produce autoreleases
+ break;
+
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ // For non-ObjC function calls, recursively analyze
+ if (MayAutorelease(cast<CallBase>(I), Depth + 1))
+ return true;
+ break;
+
+ case ARCInstKind::IntrinsicUser:
+ case ARCInstKind::User:
+ case ARCInstKind::None:
+ // These are not relevant for autorelease analysis
+ break;
+ }
+ }
+ }
+ return false;
+ }
+
+ return true;
+}
+
+/// Optimize autorelease pools by eliminating empty push/pop pairs.
+void ObjCARCOpt::OptimizeAutoreleasePools(Function &F) {
+ LLVM_DEBUG(dbgs() << "\n== ObjCARCOpt::OptimizeAutoreleasePools ==\n");
+
+ OptimizationRemarkEmitter ORE(&F);
+
+ // Process each basic block independently.
+ // TODO: Can we optimize inter-block autorelease pool pairs?
+ // This would involve tracking autorelease pool state across blocks.
+ for (BasicBlock &BB : F) {
+ // Use a stack to track nested autorelease pools
+ SmallVector<std::pair<CallInst *, bool>, 4>
+ PoolStack; // {push_inst, has_autorelease_in_scope}
+
+ for (Instruction &Inst : llvm::make_early_inc_range(BB)) {
+ ARCInstKind Class = GetBasicARCInstKind(&Inst);
+
+ switch (Class) {
+ case ARCInstKind::AutoreleasepoolPush: {
+ // Start tracking a new autorelease pool scope
+ auto *Push = cast<CallInst>(&Inst);
+ PoolStack.push_back(
+ {Push, false}); // {push_inst, has_autorelease_in_scope}
+ LLVM_DEBUG(dbgs() << "Found autorelease pool push: " << *Push << "\n");
+ break;
+ }
+
+ case ARCInstKind::AutoreleasepoolPop: {
+ auto *Pop = cast<CallInst>(&Inst);
+
+ if (PoolStack.empty())
+ break;
+
+ auto &TopPool = PoolStack.back();
+ CallInst *PendingPush = TopPool.first;
+ bool HasAutoreleaseInScope = TopPool.second;
+
+ // Pop the stack - remove this pool scope
+ PoolStack.pop_back();
+
+ // Bail if this pop doesn't match the pending push
+ if (Pop->getArgOperand(0)->stripPointerCasts() != PendingPush)
+ break;
+
+ // Bail if there were autoreleases in this scope
+ if (HasAutoreleaseInScope)
+ break;
+
+ // Optimize: eliminate this empty autorelease pool pair
+ ORE.emit([&]() {
+ return OptimizationRemark(DEBUG_TYPE, "AutoreleasePoolElimination",
+ PendingPush)
+ << "eliminated empty autorelease pool pair";
+ });
+
+ // Replace all uses of push with poison before deletion
+ PendingPush->replaceAllUsesWith(
+ PoisonValue::get(PendingPush->getType()));
+
+ Pop->eraseFromParent();
+ PendingPush->eraseFromParent();
+
+ Changed = true;
+ ++NumNoops;
+ break;
+ }
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ if (!MayAutorelease(cast<CallBase>(Inst)))
+ break;
+ LLVM_FALLTHROUGH;
+ case ARCInstKind::Autorelease:
+ case ARCInstKind::AutoreleaseRV:
+ case ARCInstKind::FusedRetainAutorelease:
+ case ARCInstKind::FusedRetainAutoreleaseRV:
+ case ARCInstKind::LoadWeak: {
+ // Track that we have autorelease calls in the current pool scope
+ if (!PoolStack.empty()) {
+ PoolStack.back().second = true; // Set has_autorelease_in_scope = true
+ LLVM_DEBUG(
+ dbgs()
+ << "Found autorelease or potential autorelease in pool scope: "
+ << Inst << "\n");
+ }
+ break;
+ }
+
+ // Enumerate all remaining ARCInstKind cases explicitly
+ case ARCInstKind::Retain:
+ case ARCInstKind::RetainRV:
+ case ARCInstKind::UnsafeClaimRV:
+ case ARCInstKind::RetainBlock:
+ case ARCInstKind::Release:
+ case ARCInstKind::NoopCast:
+ case ARCInstKind::LoadWeakRetained:
+ case ARCInstKind::StoreWeak:
+ case ARCInstKind::InitWeak:
+ case ARCInstKind::MoveWeak:
+ case ARCInstKind::CopyWeak:
+ case ARCInstKind::DestroyWeak:
+ case ARCInstKind::StoreStrong:
+ case ARCInstKind::IntrinsicUser:
+ case ARCInstKind::User:
+ case ARCInstKind::None:
+ // These instruction kinds don't affect autorelease pool optimization
+ break;
+ }
+ }
+ }
+}
+
/// @}
///
diff --git a/llvm/test/Transforms/ObjCARC/apelim.ll b/llvm/test/Transforms/ObjCARC/apelim.ll
index 2ac5d15d0df85..01179f3dec048 100644
--- a/llvm/test/Transforms/ObjCARC/apelim.ll
+++ b/llvm/test/Transforms/ObjCARC/apelim.ll
@@ -1,4 +1,4 @@
-; RUN: opt -S -passes=objc-arc-apelim < %s | FileCheck %s
+; RUN: opt -S -passes=objc-arc < %s | FileCheck %s
; rdar://10227311
@llvm.global_ctors = appending global [2 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__I_x, ptr null }, { i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__I_y, ptr null }]
diff --git a/llvm/test/Transforms/ObjCARC/comdat-ipo.ll b/llvm/test/Transforms/ObjCARC/comdat-ipo.ll
index 3f91d3bea9f14..d43804c20d936 100644
--- a/llvm/test/Transforms/ObjCARC/comdat-ipo.ll
+++ b/llvm/test/Transforms/ObjCARC/comdat-ipo.ll
@@ -1,4 +1,4 @@
-; RUN: opt -S -passes=objc-arc-apelim < %s | FileCheck %s
+; RUN: opt -S -passes=objc-arc < %s | FileCheck %s
; See PR26774
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
new file mode 100644
index 0000000000000..896717f92146f
--- /dev/null
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -0,0 +1,319 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; Test for autorelease pool optimizations
+; RUN: opt -passes=objc-arc < %s -S | FileCheck %s
+
+declare ptr @llvm.objc.autoreleasePoolPush()
+declare void @llvm.objc.autoreleasePoolPop(ptr)
+declare ptr @llvm.objc.autorelease(ptr)
+declare ptr @llvm.objc.retain(ptr)
+declare ptr @create_object()
+declare void @use_object(ptr)
+declare ptr @object_with_thing()
+declare void @opaque_callee()
+
+; Empty autorelease pool should be eliminated
+define void @test_empty_pool() {
+; CHECK-LABEL: define void @test_empty_pool() {
+; C...
[truncated]
|
@llvm/pr-subscribers-clang-codegen Author: AZero13 (AZero13) ChangesAs per the new guidelines, I finished this with the intention of being admitted into the 21.x release. It was finished for a few weeks before it was merged, and I would like to backport this. I apolgoize if this is getting too close to the merge window, but this is just in time, no? Patch is 32.47 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/150771.diff 11 Files Affected:
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 2f6d4c414e737..17d83ff5dbebe 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -1027,12 +1027,6 @@ void EmitAssemblyHelper::RunOptimizationPipeline(
MPM.addPass(
createModuleToFunctionPassAdaptor(ObjCARCExpandPass()));
});
- PB.registerPipelineEarlySimplificationEPCallback(
- [](ModulePassManager &MPM, OptimizationLevel Level,
- ThinOrFullLTOPhase) {
- if (Level != OptimizationLevel::O0)
- MPM.addPass(ObjCARCAPElimPass());
- });
PB.registerScalarOptimizerLateEPCallback(
[](FunctionPassManager &FPM, OptimizationLevel Level) {
if (Level != OptimizationLevel::O0)
diff --git a/llvm/include/llvm/Transforms/ObjCARC.h b/llvm/include/llvm/Transforms/ObjCARC.h
index c927513469a35..c4b4c4f0b09c6 100644
--- a/llvm/include/llvm/Transforms/ObjCARC.h
+++ b/llvm/include/llvm/Transforms/ObjCARC.h
@@ -35,10 +35,6 @@ struct ObjCARCContractPass : public PassInfoMixin<ObjCARCContractPass> {
LLVM_ABI PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};
-struct ObjCARCAPElimPass : public PassInfoMixin<ObjCARCAPElimPass> {
- LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
-};
-
struct ObjCARCExpandPass : public PassInfoMixin<ObjCARCExpandPass> {
LLVM_ABI PreservedAnalyses run(Function &M, FunctionAnalysisManager &AM);
};
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 9a943155aa19f..8d86e2b996449 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -119,7 +119,6 @@ MODULE_PASS("module-inline", ModuleInlinerPass())
MODULE_PASS("name-anon-globals", NameAnonGlobalPass())
MODULE_PASS("no-op-module", NoOpModulePass())
MODULE_PASS("nsan", NumericalStabilitySanitizerPass())
-MODULE_PASS("objc-arc-apelim", ObjCARCAPElimPass())
MODULE_PASS("openmp-opt", OpenMPOptPass())
MODULE_PASS("openmp-opt-postlink",
OpenMPOptPass(ThinOrFullLTOPhase::FullLTOPostLink))
diff --git a/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h b/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
index 3fa844eda21cf..6135c7b938a3e 100644
--- a/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
+++ b/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
@@ -46,6 +46,8 @@ enum class ARCRuntimeEntryPointKind {
UnsafeClaimRV,
RetainAutorelease,
RetainAutoreleaseRV,
+ AutoreleasePoolPush,
+ AutoreleasePoolPop,
};
/// Declarations for ObjC runtime functions and constants. These are initialized
@@ -67,6 +69,8 @@ class ARCRuntimeEntryPoints {
UnsafeClaimRV = nullptr;
RetainAutorelease = nullptr;
RetainAutoreleaseRV = nullptr;
+ AutoreleasePoolPush = nullptr;
+ AutoreleasePoolPop = nullptr;
}
Function *get(ARCRuntimeEntryPointKind kind) {
@@ -101,6 +105,12 @@ class ARCRuntimeEntryPoints {
case ARCRuntimeEntryPointKind::RetainAutoreleaseRV:
return getIntrinsicEntryPoint(RetainAutoreleaseRV,
Intrinsic::objc_retainAutoreleaseReturnValue);
+ case ARCRuntimeEntryPointKind::AutoreleasePoolPush:
+ return getIntrinsicEntryPoint(AutoreleasePoolPush,
+ Intrinsic::objc_autoreleasePoolPush);
+ case ARCRuntimeEntryPointKind::AutoreleasePoolPop:
+ return getIntrinsicEntryPoint(AutoreleasePoolPop,
+ Intrinsic::objc_autoreleasePoolPop);
}
llvm_unreachable("Switch should be a covered switch.");
@@ -143,6 +153,12 @@ class ARCRuntimeEntryPoints {
/// Declaration for objc_retainAutoreleaseReturnValue().
Function *RetainAutoreleaseRV = nullptr;
+ /// Declaration for objc_autoreleasePoolPush().
+ Function *AutoreleasePoolPush = nullptr;
+
+ /// Declaration for objc_autoreleasePoolPop().
+ Function *AutoreleasePoolPop = nullptr;
+
Function *getIntrinsicEntryPoint(Function *&Decl, Intrinsic::ID IntID) {
if (Decl)
return Decl;
diff --git a/llvm/lib/Transforms/ObjCARC/CMakeLists.txt b/llvm/lib/Transforms/ObjCARC/CMakeLists.txt
index 80867dbc270d7..4274667a2c2b7 100644
--- a/llvm/lib/Transforms/ObjCARC/CMakeLists.txt
+++ b/llvm/lib/Transforms/ObjCARC/CMakeLists.txt
@@ -2,7 +2,6 @@ add_llvm_component_library(LLVMObjCARCOpts
ObjCARC.cpp
ObjCARCOpts.cpp
ObjCARCExpand.cpp
- ObjCARCAPElim.cpp
ObjCARCContract.cpp
DependencyAnalysis.cpp
ProvenanceAnalysis.cpp
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCAPElim.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCAPElim.cpp
deleted file mode 100644
index dceb2ebb1863e..0000000000000
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCAPElim.cpp
+++ /dev/null
@@ -1,156 +0,0 @@
-//===- ObjCARCAPElim.cpp - ObjC ARC Optimization --------------------------===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-/// \file
-///
-/// This file defines ObjC ARC optimizations. ARC stands for Automatic
-/// Reference Counting and is a system for managing reference counts for objects
-/// in Objective C.
-///
-/// This specific file implements optimizations which remove extraneous
-/// autorelease pools.
-///
-/// WARNING: This file knows about certain library functions. It recognizes them
-/// by name, and hardwires knowledge of their semantics.
-///
-/// WARNING: This file knows about how certain Objective-C library functions are
-/// used. Naive LLVM IR transformations which would otherwise be
-/// behavior-preserving may break these assumptions.
-///
-//===----------------------------------------------------------------------===//
-
-#include "llvm/ADT/STLExtras.h"
-#include "llvm/Analysis/ObjCARCAnalysisUtils.h"
-#include "llvm/Analysis/ObjCARCInstKind.h"
-#include "llvm/IR/Constants.h"
-#include "llvm/IR/InstrTypes.h"
-#include "llvm/IR/PassManager.h"
-#include "llvm/Support/Debug.h"
-#include "llvm/Support/raw_ostream.h"
-#include "llvm/Transforms/ObjCARC.h"
-
-using namespace llvm;
-using namespace llvm::objcarc;
-
-#define DEBUG_TYPE "objc-arc-ap-elim"
-
-namespace {
-
-/// Interprocedurally determine if calls made by the given call site can
-/// possibly produce autoreleases.
-bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) {
- if (const Function *Callee = CB.getCalledFunction()) {
- if (!Callee->hasExactDefinition())
- return true;
- for (const BasicBlock &BB : *Callee) {
- for (const Instruction &I : BB)
- if (const CallBase *JCB = dyn_cast<CallBase>(&I))
- // This recursion depth limit is arbitrary. It's just great
- // enough to cover known interesting testcases.
- if (Depth < 3 && !JCB->onlyReadsMemory() &&
- MayAutorelease(*JCB, Depth + 1))
- return true;
- }
- return false;
- }
-
- return true;
-}
-
-bool OptimizeBB(BasicBlock *BB) {
- bool Changed = false;
-
- Instruction *Push = nullptr;
- for (Instruction &Inst : llvm::make_early_inc_range(*BB)) {
- switch (GetBasicARCInstKind(&Inst)) {
- case ARCInstKind::AutoreleasepoolPush:
- Push = &Inst;
- break;
- case ARCInstKind::AutoreleasepoolPop:
- // If this pop matches a push and nothing in between can autorelease,
- // zap the pair.
- if (Push && cast<CallInst>(&Inst)->getArgOperand(0) == Push) {
- Changed = true;
- LLVM_DEBUG(dbgs() << "ObjCARCAPElim::OptimizeBB: Zapping push pop "
- "autorelease pair:\n"
- " Pop: "
- << Inst << "\n"
- << " Push: " << *Push
- << "\n");
- Inst.eraseFromParent();
- Push->eraseFromParent();
- }
- Push = nullptr;
- break;
- case ARCInstKind::CallOrUser:
- if (MayAutorelease(cast<CallBase>(Inst)))
- Push = nullptr;
- break;
- default:
- break;
- }
- }
-
- return Changed;
-}
-
-bool runImpl(Module &M) {
- if (!EnableARCOpts)
- return false;
-
- // If nothing in the Module uses ARC, don't do anything.
- if (!ModuleHasARC(M))
- return false;
- // Find the llvm.global_ctors variable, as the first step in
- // identifying the global constructors. In theory, unnecessary autorelease
- // pools could occur anywhere, but in practice it's pretty rare. Global
- // ctors are a place where autorelease pools get inserted automatically,
- // so it's pretty common for them to be unnecessary, and it's pretty
- // profitable to eliminate them.
- GlobalVariable *GV = M.getGlobalVariable("llvm.global_ctors");
- if (!GV)
- return false;
-
- assert(GV->hasDefinitiveInitializer() &&
- "llvm.global_ctors is uncooperative!");
-
- bool Changed = false;
-
- // Dig the constructor functions out of GV's initializer.
- ConstantArray *Init = cast<ConstantArray>(GV->getInitializer());
- for (User::op_iterator OI = Init->op_begin(), OE = Init->op_end();
- OI != OE; ++OI) {
- Value *Op = *OI;
- // llvm.global_ctors is an array of three-field structs where the second
- // members are constructor functions.
- Function *F = dyn_cast<Function>(cast<ConstantStruct>(Op)->getOperand(1));
- // If the user used a constructor function with the wrong signature and
- // it got bitcasted or whatever, look the other way.
- if (!F)
- continue;
- // Only look at function definitions.
- if (F->isDeclaration())
- continue;
- // Only look at functions with one basic block.
- if (std::next(F->begin()) != F->end())
- continue;
- // Ok, a single-block constructor function definition. Try to optimize it.
- Changed |= OptimizeBB(&F->front());
- }
-
- return Changed;
-}
-
-} // namespace
-
-PreservedAnalyses ObjCARCAPElimPass::run(Module &M, ModuleAnalysisManager &AM) {
- if (!runImpl(M))
- return PreservedAnalyses::all();
- PreservedAnalyses PA;
- PA.preserveSet<CFGAnalyses>();
- return PA;
-}
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index 5eb3f51d38945..66a2c7632aadc 100644
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
+++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
@@ -39,6 +39,7 @@
#include "llvm/Analysis/ObjCARCAnalysisUtils.h"
#include "llvm/Analysis/ObjCARCInstKind.h"
#include "llvm/Analysis/ObjCARCUtil.h"
+#include "llvm/Analysis/OptimizationRemarkEmitter.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/CFG.h"
#include "llvm/IR/Constant.h"
@@ -132,11 +133,8 @@ static const Value *FindSingleUseIdentifiedObject(const Value *Arg) {
//
// The second retain and autorelease can be deleted.
-// TODO: It should be possible to delete
-// objc_autoreleasePoolPush and objc_autoreleasePoolPop
-// pairs if nothing is actually autoreleased between them. Also, autorelease
-// calls followed by objc_autoreleasePoolPop calls (perhaps in ObjC++ code
-// after inlining) can be turned into plain release calls.
+// TODO: Autorelease calls followed by objc_autoreleasePoolPop calls (perhaps in
+// ObjC++ code after inlining) can be turned into plain release calls.
// TODO: Critical-edge splitting. If the optimial insertion point is
// a critical edge, the current algorithm has to fail, because it doesn't
@@ -566,6 +564,8 @@ class ObjCARCOpt {
void OptimizeReturns(Function &F);
+ void OptimizeAutoreleasePools(Function &F);
+
template <typename PredicateT>
static void cloneOpBundlesIf(CallBase *CI,
SmallVectorImpl<OperandBundleDef> &OpBundles,
@@ -2473,6 +2473,11 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) {
(1 << unsigned(ARCInstKind::AutoreleaseRV))))
OptimizeReturns(F);
+ // Optimizations for autorelease pools.
+ if (UsedInThisFunction & ((1 << unsigned(ARCInstKind::AutoreleasepoolPush)) |
+ (1 << unsigned(ARCInstKind::AutoreleasepoolPop))))
+ OptimizeAutoreleasePools(F);
+
// Gather statistics after optimization.
#ifndef NDEBUG
if (AreStatisticsEnabled()) {
@@ -2485,6 +2490,183 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) {
return Changed;
}
+/// Interprocedurally determine if calls made by the given call site can
+/// possibly produce autoreleases.
+bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) {
+ if (CB.onlyReadsMemory())
+ return false;
+
+ // This recursion depth limit is arbitrary. It's just great
+ // enough to cover known interesting testcases.
+ if (Depth > 5)
+ return true;
+
+ if (const Function *Callee = CB.getCalledFunction()) {
+ if (!Callee->hasExactDefinition())
+ return true;
+ for (const BasicBlock &BB : *Callee) {
+ for (const Instruction &I : BB) {
+ // TODO: Ignore all instructions between autorelease pools
+ ARCInstKind InstKind = GetBasicARCInstKind(&I);
+ switch (InstKind) {
+ case ARCInstKind::Autorelease:
+ case ARCInstKind::AutoreleaseRV:
+ case ARCInstKind::FusedRetainAutorelease:
+ case ARCInstKind::FusedRetainAutoreleaseRV:
+ case ARCInstKind::LoadWeak:
+ // These may produce autoreleases
+ return true;
+
+ case ARCInstKind::Retain:
+ case ARCInstKind::RetainRV:
+ case ARCInstKind::UnsafeClaimRV:
+ case ARCInstKind::RetainBlock:
+ case ARCInstKind::Release:
+ case ARCInstKind::NoopCast:
+ case ARCInstKind::LoadWeakRetained:
+ case ARCInstKind::StoreWeak:
+ case ARCInstKind::InitWeak:
+ case ARCInstKind::MoveWeak:
+ case ARCInstKind::CopyWeak:
+ case ARCInstKind::DestroyWeak:
+ case ARCInstKind::StoreStrong:
+ case ARCInstKind::AutoreleasepoolPush:
+ case ARCInstKind::AutoreleasepoolPop:
+ // These ObjC runtime functions don't produce autoreleases
+ break;
+
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ // For non-ObjC function calls, recursively analyze
+ if (MayAutorelease(cast<CallBase>(I), Depth + 1))
+ return true;
+ break;
+
+ case ARCInstKind::IntrinsicUser:
+ case ARCInstKind::User:
+ case ARCInstKind::None:
+ // These are not relevant for autorelease analysis
+ break;
+ }
+ }
+ }
+ return false;
+ }
+
+ return true;
+}
+
+/// Optimize autorelease pools by eliminating empty push/pop pairs.
+void ObjCARCOpt::OptimizeAutoreleasePools(Function &F) {
+ LLVM_DEBUG(dbgs() << "\n== ObjCARCOpt::OptimizeAutoreleasePools ==\n");
+
+ OptimizationRemarkEmitter ORE(&F);
+
+ // Process each basic block independently.
+ // TODO: Can we optimize inter-block autorelease pool pairs?
+ // This would involve tracking autorelease pool state across blocks.
+ for (BasicBlock &BB : F) {
+ // Use a stack to track nested autorelease pools
+ SmallVector<std::pair<CallInst *, bool>, 4>
+ PoolStack; // {push_inst, has_autorelease_in_scope}
+
+ for (Instruction &Inst : llvm::make_early_inc_range(BB)) {
+ ARCInstKind Class = GetBasicARCInstKind(&Inst);
+
+ switch (Class) {
+ case ARCInstKind::AutoreleasepoolPush: {
+ // Start tracking a new autorelease pool scope
+ auto *Push = cast<CallInst>(&Inst);
+ PoolStack.push_back(
+ {Push, false}); // {push_inst, has_autorelease_in_scope}
+ LLVM_DEBUG(dbgs() << "Found autorelease pool push: " << *Push << "\n");
+ break;
+ }
+
+ case ARCInstKind::AutoreleasepoolPop: {
+ auto *Pop = cast<CallInst>(&Inst);
+
+ if (PoolStack.empty())
+ break;
+
+ auto &TopPool = PoolStack.back();
+ CallInst *PendingPush = TopPool.first;
+ bool HasAutoreleaseInScope = TopPool.second;
+
+ // Pop the stack - remove this pool scope
+ PoolStack.pop_back();
+
+ // Bail if this pop doesn't match the pending push
+ if (Pop->getArgOperand(0)->stripPointerCasts() != PendingPush)
+ break;
+
+ // Bail if there were autoreleases in this scope
+ if (HasAutoreleaseInScope)
+ break;
+
+ // Optimize: eliminate this empty autorelease pool pair
+ ORE.emit([&]() {
+ return OptimizationRemark(DEBUG_TYPE, "AutoreleasePoolElimination",
+ PendingPush)
+ << "eliminated empty autorelease pool pair";
+ });
+
+ // Replace all uses of push with poison before deletion
+ PendingPush->replaceAllUsesWith(
+ PoisonValue::get(PendingPush->getType()));
+
+ Pop->eraseFromParent();
+ PendingPush->eraseFromParent();
+
+ Changed = true;
+ ++NumNoops;
+ break;
+ }
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ if (!MayAutorelease(cast<CallBase>(Inst)))
+ break;
+ LLVM_FALLTHROUGH;
+ case ARCInstKind::Autorelease:
+ case ARCInstKind::AutoreleaseRV:
+ case ARCInstKind::FusedRetainAutorelease:
+ case ARCInstKind::FusedRetainAutoreleaseRV:
+ case ARCInstKind::LoadWeak: {
+ // Track that we have autorelease calls in the current pool scope
+ if (!PoolStack.empty()) {
+ PoolStack.back().second = true; // Set has_autorelease_in_scope = true
+ LLVM_DEBUG(
+ dbgs()
+ << "Found autorelease or potential autorelease in pool scope: "
+ << Inst << "\n");
+ }
+ break;
+ }
+
+ // Enumerate all remaining ARCInstKind cases explicitly
+ case ARCInstKind::Retain:
+ case ARCInstKind::RetainRV:
+ case ARCInstKind::UnsafeClaimRV:
+ case ARCInstKind::RetainBlock:
+ case ARCInstKind::Release:
+ case ARCInstKind::NoopCast:
+ case ARCInstKind::LoadWeakRetained:
+ case ARCInstKind::StoreWeak:
+ case ARCInstKind::InitWeak:
+ case ARCInstKind::MoveWeak:
+ case ARCInstKind::CopyWeak:
+ case ARCInstKind::DestroyWeak:
+ case ARCInstKind::StoreStrong:
+ case ARCInstKind::IntrinsicUser:
+ case ARCInstKind::User:
+ case ARCInstKind::None:
+ // These instruction kinds don't affect autorelease pool optimization
+ break;
+ }
+ }
+ }
+}
+
/// @}
///
diff --git a/llvm/test/Transforms/ObjCARC/apelim.ll b/llvm/test/Transforms/ObjCARC/apelim.ll
index 2ac5d15d0df85..01179f3dec048 100644
--- a/llvm/test/Transforms/ObjCARC/apelim.ll
+++ b/llvm/test/Transforms/ObjCARC/apelim.ll
@@ -1,4 +1,4 @@
-; RUN: opt -S -passes=objc-arc-apelim < %s | FileCheck %s
+; RUN: opt -S -passes=objc-arc < %s | FileCheck %s
; rdar://10227311
@llvm.global_ctors = appending global [2 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__I_x, ptr null }, { i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__I_y, ptr null }]
diff --git a/llvm/test/Transforms/ObjCARC/comdat-ipo.ll b/llvm/test/Transforms/ObjCARC/comdat-ipo.ll
index 3f91d3bea9f14..d43804c20d936 100644
--- a/llvm/test/Transforms/ObjCARC/comdat-ipo.ll
+++ b/llvm/test/Transforms/ObjCARC/comdat-ipo.ll
@@ -1,4 +1,4 @@
-; RUN: opt -S -passes=objc-arc-apelim < %s | FileCheck %s
+; RUN: opt -S -passes=objc-arc < %s | FileCheck %s
; See PR26774
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
new file mode 100644
index 0000000000000..896717f92146f
--- /dev/null
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -0,0 +1,319 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; Test for autorelease pool optimizations
+; RUN: opt -passes=objc-arc < %s -S | FileCheck %s
+
+declare ptr @llvm.objc.autoreleasePoolPush()
+declare void @llvm.objc.autoreleasePoolPop(ptr)
+declare ptr @llvm.objc.autorelease(ptr)
+declare ptr @llvm.objc.retain(ptr)
+declare ptr @create_object()
+declare void @use_object(ptr)
+declare ptr @object_with_thing()
+declare void @opaque_callee()
+
+; Empty autorelease pool should be eliminated
+define void @test_empty_pool() {
+; CHECK-LABEL: define void @test_empty_pool() {
+; C...
[truncated]
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be backported.
I would most likely defer to @nikic here, but can you give us a good case for merging this into the release? |
A few reasons:
|
…lvm#144788) Erase empty autorelease pools that have no autorelease in them
ObjCARCAPElimPass has been made obsolete now that we remove unused autorelease pools.
@nikic What do you think? do you have strong feelings about merging this, I would normally agree with you, but since we moved the date forward and it's still early in the release process I don't mind as much. |
Yes, I feel strongly that this should not be backported. This patch is both unimportant and risky. |
As per the new guidelines, I finished this with the intention of being admitted into the 21.x release.
It was finished for a few weeks before it was merged, and I would like to backport this. I apolgoize if this is getting too close to the merge window, but this is just in time, no?