As far as I can tell, rustc doesn't mark functions as NoUnwind/NEVER_UNWIND, apart from a handful of broad generalizations like panic=abort or extern "not *-unwind". I saw some scan of function bodies for coroutines, but not for regular functions.
The problem is that when a Drop::drop can't be inlined, it ends up being a potentially-unwinding call, and because unwinding calls can cause more drops, and panics during unwinding are not allowed, every drop then also generates an unwinding landing pad with and a call to panicking::panic_in_cleanup. This basically doubles the amount of drop-related code, and LLVM won't move nor unify calls to unwinding functions.
This affects Arc. It intentionally has a non-inlined drop_slow, which triggers the unwinding bloat:
I am surprised that LLVM doesn't seem to deduce "nounwind" itself. Maybe it's because drop_slow has external linkage and gets unwind tables generated, so LLVM can't trust it won't be replaced by the linker with a different function that unwinds? but I don't see LTO making a difference.
It should be beneficial to have a MIR opt pass that marks functions as "nounwind" if they don't call any potentially-unwinding functions themselves.
It usually does, so I would suggest poking at this more. There might just be a bug somewhere that could be fixed -- especially if even LTO doesn't solve it.
I think it's because rustc emits invoke in the IR, which implies unwinding. LLVM's pass for adding nounwind uses:
/// Helper for NoUnwind inference predicate InstrBreaksAttribute.
static bool InstrBreaksNonThrowing(Instruction &I, const SCCNodeSet &SCCNodes) {
if (!I.mayThrow(/* IncludePhaseOneUnwind */ true))
return false;
if (const auto *CI = dyn_cast<CallInst>(&I)) {
if (Function *Callee = CI->getCalledFunction()) {
// I is a may-throw call to a function inside our SCC. This doesn't
// invalidate our current working assumption that the SCC is no-throw; we
// just have to scan that other function.
if (SCCNodes.contains(Callee))
return false;
}
}
return true;
}
which only allows call (CallInst).
This check is relevant for recursive functions. I happen to have Arc that is a recursive type, so Arc::drop_slow calls drop_in_place which may call Arc::drop_slow again. In this case rustc assuming that drop_slow may unwind makes it emit unwind-requiring invoke instruction when called recursively, which then cements its status as an unwinding function.
I don't know if LLVM checking for CallInst is intentional, or was it meant to be CallBase (which would include InstInvoke)