[llvm] [IPSCCP] Track returns of non-interposable definitions (PR #210374)

via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 17 09:51:07 PDT 2026


michaelselehov wrote:

## [IPSCCP] Track returns of non-interposable definitions

### Summary

`canTrackReturnsInterprocedurally()` gated interprocedural return-value tracking on `hasExactDefinition()`. That predicate is `false` for weak-for-linker ODR linkages (`linkonce_odr` / `weak_odr`) because their bodies *may be derefined* at link time. As a consequence IPSCCP never attached inferred return attributes (a `range`, a constant, or any lattice fact) to such functions, even though ODR guarantees every emitted copy is semantically equivalent and the symbol is **not** interposable.

This patch relaxes the gate to accept any non-interposable definition, while keeping the exclusions that matter for soundness.

### Why this surfaced with (Thin/Full)LTO

In a traditional per-TU compile, the small C++ accessor wrappers that sit in front of a target intrinsic are almost always inlined locally, or the front-end/early cleanup internalizes them, so the value flows straight from the intrinsic (which already carries a bounded `range`) into the consumer. The information never has to survive as a *return attribute* on a `linkonce_odr` symbol.

Under LTO the pipeline changes in two ways:

- Inlining and specialization decisions are deferred to the combined module, so at the point IPSCCP runs, the value still flows *through* the `linkonce_odr` accessor as a real call/return edge rather than an already-inlined body.
- The full-LTO/offload driver path no longer aggressively internalizes these ODR wrappers the way the older single-module flow effectively did, so they retain their `linkonce_odr` linkage into the interprocedural passes.

The net effect: a bounded `range` that is present on the underlying definition is available at the accessor's return, but IPSCCP refuses to record it purely because of the `linkonce_odr` linkage. Downstream, without that range the optimizer cannot prove the value is non-negative / in-bounds, so it drops `inbounds`/`nuw` on the strided address computations and the backend materializes extra 64-bit address arithmetic. That is a real, measurable code-quality regression on hot memory-bandwidth kernels, and it only appears once these wrappers survive as ODR symbols into IPO — i.e. with LTO.

### Why this fix

The unnecessary restriction is the use of `hasExactDefinition()`. "Exact definition" is a strong property — it means the body seen here is byte-for-byte the one that will run. That is stricter than what return-value inference actually needs.

For a return-value fact, the relevant question is only: *does every definition the linker might select produce a return value consistent with what we inferred from this body?* For non-interposable ODR linkages the answer is yes:

- ODR requires all definitions to be semantically equivalent, and LLVM's "derefinement" only permits replacement by a **refinement** of the same source function. Refinement can remove undefined behavior but cannot introduce new observable return values under defined behavior, so any *may-facts* over the returned value (ranges, "returns a constant", known bits) computed from one copy remain valid for whichever copy is kept.
- `available_externally` is covered by the same argument: the body must match the external definition.

So the correct predicate is "a non-interposable definition", i.e. `!isDeclaration() && !isInterposable()`, rather than "an exact definition".

Two exclusions are preserved deliberately:

- **Interposable linkages** (`linkonce_any` / `weak_any` / `common` / `extern_weak`): the linker may substitute an entirely unrelated body, so no fact from the visible body is valid. `isInterposable()` already captures exactly this set.
- **`nobuiltin` definitions**: call sites may be marked `builtin` and are allowed to assume the standard-library semantics of the function rather than the visible body. Propagating a fact derived from the body into such a call site would be unsound. `hasExactDefinition()` excluded these via `mayBeDerefined()`, so we re-add the check explicitly (`!hasFnAttribute(Attribute::NoBuiltin)`).

### Scope / safety

This change only broadens *which functions get their returns tracked*; it does not enable any new rewrite of the function itself. The unsafe transforms remain independently gated:

- Return-value zapping / dead-return removal goes through argument-tracked functions, which still require local linkage (`hasLocalLinkage()`), no address taken, and an exact definition — none of which are loosened here.
- Function specialization / dead-original removal keep their own non-interposability + argument-tracking + no-address-taken conditions.

### Tests

- `llvm/test/Transforms/SCCP/ipsccp-odr-return-range.ll` (new): a `range` return attribute is inferred for `linkonce_odr`, `weak_odr`, and `available_externally`, and is **not** inferred for the interposable `linkonce_any` / `weak_any` or for a `nobuiltin` definition.
- `llvm/test/Transforms/SCCP/comdat-ipo.ll` (updated): a constant return is now propagated through a `linkonce_odr` callee into its caller, while an interposable `linkonce_any` callee still blocks propagation. The stale comment (which asserted ODR returns can never be propagated) is corrected.

Full `check-llvm` passes.

Assisted-by: Claude Opus


https://github.com/llvm/llvm-project/pull/210374


More information about the llvm-commits mailing list