llvm / llvm/llvm-project

[X86][CodeGen] Excessive CFI complexity on i386 with -fomit-frame-pointer causes slow exception unwinding

Open
#215,319 1 comment 0 reactions 0 assignees View on GitHub
backend:X86 llvm:codegen
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

On i386 with `-fomit-frame-pointer`, Clang generates push/pop sequences for call arguments in exception-handling paths, resulting in excessive CFI directives (DW_CFA_GNU_args_size, CFA offset adjustments). GCC instead pre-allocates stack space and uses `mov` to place arguments, keeping the CFA offset constant through the exception handling path.

This causes significantly slower DWARF-based exception unwinding at runtime (2-5ms vs <1ms with GCC for equivalent code), because the unwinder must interpret a more complex CFI program at every frame.

## Reproducer

```cpp
struct StopException {};

bool poll_message();
void signal_abort();

void with_fix(bool scan_active)
{
try {
while (poll_message()) {}
}
catch (...) {
if (scan_active)
signal_abort();
throw;
}
}
```

## Compile command

```
clang++ -S --target=i386 -fexceptions -fomit-frame-pointer -O2 -march=pentium4 -o test.s test.cpp
```

**Clang version:** Reproduces from at least 18.1.8 through trunk (clang 22)

## Observed behavior

Clang emits push/pop for arguments in the landing pad and rethrow path, requiring per-call CFI updates:

```asm
.Ltmp2:
.cfi_escape 0x2e, 0x04
pushl %eax
.cfi_adjust_cfa_offset 4
calll __cxa_begin_catch
addl $4, %esp
.cfi_adjust_cfa_offset -4
...
.cfi_escape 0x2e, 0x04
pushl %esi
.cfi_adjust_cfa_offset 4
calll _Unwind_Resume@PLT
```

This results in 8 CFI directives in the exception handling path, including multiple `.cfi_escape 0x2e` (DW_CFA_GNU_args_size) sequences.

## Expected behavior (GCC's approach)

GCC pre-allocates stack space at function entry (`subl $24, %esp`) and uses `mov` to place arguments. The CFA offset is constant throughout the exception handling path — no `.cfi_escape` or `.cfi_adjust_cfa_offset` churn:

```asm
.L13:
movl %eax, (%esp)
call __cxa_begin_catch
...
movl %ebx, (%esp)
call _Unwind_Resume
```

GCC's LSDA also has 3 call sites vs Clang's 5 call sites.

## Impact

On i386 targets without frame pointers (embedded/RTOS environments), the DWARF unwinder must evaluate the full CFI program to unwind each frame. The extra CFI directives make unwinding measurably slower — in our case causing a timing-sensitive operation to exceed a 1ms deadline and trigger a secondary error.

The push/pop codegen pattern is the root cause — it necessitates the complex CFI. With pre-allocated stack slots (GCC's approach), the CFA offset is established once and remains constant, making the DWARF program trivial to evaluate.

## Suggested fix

In exception-handling landing pad paths on i386, prefer pre-allocated stack slots over push/pop for placing call arguments. This keeps the CFI state constant and reduces DWARF evaluation cost during unwinding.

## Related

- RFC: Add "call unwindabort" to LLVM IR: https://discourse.llvm.org/t/rfc-add-call-unwindabort-to-llvm-ir/62543
- Code generation for noexcept functions: https://discourse.llvm.org/t/code-generation-for-noexcept-functions/32072
- Clang also generates an extra `__clang_call_terminate` landing pad that GCC avoids entirely (GCC relies on the personality function to terminate for unhandled exceptions during cleanup)

Contributor guide

Open the contributing guide

Research direction

Start by running the provided clang++ i386 compilation command and comparing the generated exception-handling assembly and CFI with GCC's output. Trace the LLVM code-generation path for i386 landing pads and call-argument setup, then add or run a focused regression test if the relevant test location is identified. Done means equivalent code uses simpler, stable CFI in the described paths without regressing exception handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.