Code bloat caused by lack of nounwind attr on `drop_in_place`

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:

4 Likes

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.

2 Likes

In my case it's been made worse by struct + Drop + ? creating quadratically growing code bloat.

3 Likes

There are proposals to abort when drop panics, at which point all drop functions could be marked as non unwinding.

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.

1 Like

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)

1 Like