Skip to content

Conversation

@eeckstein
Copy link
Contributor

So far hoisting destroys (destroy_value and destroy_addr) in the optimizer worked differently for lexical and non-lexical lifetimes: lexical lifetimes need to respect deinit-barriers, while destroys of non-lexical lifetimes could be moved over deinit-barriers.

This had two problems:

  • The behavior of a compiled program can be different between Onone and optimized builds.
  • It is complicated to correctly maintain the "is-lexical" flags in SIL throughout the optimizer pipeline. Even worse, some optimization do not run in cases where it's difficult to maintain the flags.

This PR

  • introduces a new MandatoryDestroyHoisting pass which hoists destroys of non-lexical lifetimes in the mandatory pipeline
  • makes all destroy-hoisting passes in the optimizer respect deinit-barriers for all lifetimes, including non-lexical lifetimes.

This almost guarantees consistent behavior for Onone and optimized builds. Almost, because there is still one exception for COW types (like Arrays). Such values are not lexical, but MandatoryDestroyHoisting does not hoist destroys of such variables in debug builds to not compromise debug info.

@eeckstein
Copy link
Contributor Author

@swift-ci test

@eeckstein
Copy link
Contributor Author

@swift-ci apple silicon benchmark

@atrick atrick requested a review from kavon November 5, 2025 16:40
@rjmccall
Copy link
Contributor

rjmccall commented Nov 5, 2025

The goal here is to just provide this optimization at -Onone rather than to guarantee some semantic effect? So it's okay that it doesn't run in bootstrap.

@meg-gupta
Copy link
Contributor

meg-gupta commented Nov 5, 2025

Curious why you saw this

The behavior of a compiled program can be different between Onone and optimized builds.

Non lexical lifetimes should not have observable program behavior. Was this because we did not correctly mark a value as lexical or lost this information during transformations? Is there a test showing this?

@eeckstein
Copy link
Contributor Author

The goal here is to just provide this optimization at -Onone rather than to guarantee some semantic effect? So it's okay that it doesn't run in bootstrap.

right

@eeckstein
Copy link
Contributor Author

Non lexical lifetimes should not have observable program behavior.

They do. For example, a temporary class reference can have a side effect in it's deinit. If such a temporary value is moved over a deinit barrier, it's changing the program behavior.

@eeckstein
Copy link
Contributor Author

@swift-ci test macos

@eeckstein
Copy link
Contributor Author

@swift-ci test windows

@eeckstein
Copy link
Contributor Author

@swift-ci test Windows

@eeckstein
Copy link
Contributor Author

@swift-ci smoke test Windows

@atrick
Copy link
Contributor

atrick commented Nov 6, 2025

For diagnostics, we need to concept of "this value's lifetime represents the scope of a variable declaration". We should have a common way of checking for that. We have Instruction.findVarDecl(). So for DestroyHoisting at -Onone, it might be more clear and robust to check that rather than checking for lexical lifetimes and debug users.

@atrick
Copy link
Contributor

atrick commented Nov 6, 2025

We currently build the standard library in a special mode that disables lexical lifetimes. It's important that we can eventually eliminate the special mode for the sanity of both standard library and compiler engineers.

https://github.com/apple/swift/blob/main/stdlib/cmake/modules/AddSwiftStdlib.cmake#L2146

A good test of whether this DestroyHoisting pass does not create performance problems will be if we can remove
-enable-lexical-lifetimes=false from the stdlib build.

My main concern is that, with this approach, we won't be able to optimize Array/String after inlining generic code.

…ship-transitioning instructions

except `copy_value`
It hoists `destroy_value` instructions for non-lexical values.

```
  %1 = some_ownedValue
  ...
  last_use(%1)
  ... // other instructions
  destroy_value %1
```
->
```
  %1 = some_ownedValue
  ...
  last_use(%1)
  destroy_value %1    // <- moved after the last use
  ... // other instructions
```

In contrast to non-mandatory optimization passes, this is the only pass which hoists destroys over deinit-barriers.
This ensures consistent behavior in -Onone and optimized builds.
@eeckstein eeckstein force-pushed the mandatory-destroy-hoisting branch from 2aeabbf to 04688a6 Compare November 6, 2025 20:01
@eeckstein
Copy link
Contributor Author

@swift-ci smoke test

Copy link
Contributor

@atrick atrick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I am concerned about general long-term performance and specifically that this will make it harder to enable lexical lifetimes in the standard library because we won't be able to optimize array/string after specialization and inlining.

@atrick
Copy link
Contributor

atrick commented Nov 6, 2025

Answering my own previous question... in the future we could probably teach optimizations about @eagerMove types so we could still optimize Array/String lifetimes even without the lexical flag on an instruction.

@eeckstein eeckstein merged commit 552b665 into swiftlang:main Nov 7, 2025
3 checks passed
@eeckstein eeckstein deleted the mandatory-destroy-hoisting branch November 7, 2025 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants