Skip to content

[OwnershipUtils] Classify moves from limited-use values as redundant. #64448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion include/swift/SIL/OwnershipUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -1349,10 +1349,17 @@ bool isNestedLexicalBeginBorrow(BeginBorrowInst *bbi);

/// Whether specified move_value is redundant.
///
/// A move_value is redundant if the lifetimes that it separates both have the
/// A move_value is redundant if it doesn't
/// - alter constraints (lexicality, ownership)
/// - enable optimizations (e.g, separate smaller scopes within which a value
/// escapes)
///
/// For example, if the lifetimes that a move_value separates both have the
/// same characteristics with respect to
/// - ownership
/// - lexicality
/// - escaping
/// then the move_value is redundant.
bool isRedundantMoveValue(MoveValueInst *mvi);

} // namespace swift
Expand Down
51 changes: 46 additions & 5 deletions lib/SIL/Utils/OwnershipUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2383,21 +2383,62 @@ bool swift::isNestedLexicalBeginBorrow(BeginBorrowInst *bbi) {
}

bool swift::isRedundantMoveValue(MoveValueInst *mvi) {
// Check whether the moved-from value's lifetime and the moved-to value's
// lifetime have the same (1) ownership, (2) lexicality, and (3) escaping.
//
// Along the way, also check for cases where they have different values for
// those characteristics but it doesn't matter because of how limited the uses
// of the original value are (for now, whether the move is the only use).

auto original = mvi->getOperand();

// If the moved-from value has none ownership, hasPointerEscape can't handle
// it, so it can't be used to determine whether escaping matches.
// (1) Ownership matches?
// (The new value always has owned ownership.)
if (original->getOwnershipKind() != OwnershipKind::Owned) {
return false;
}

// First, check whether lexicality matches, the cheaper check.
// (2) Lexicality matches?
if (mvi->isLexical() != original->isLexical()) {
return false;
}

// Then, check whether escaping matches, the more expensive check.
if (hasPointerEscape(mvi) != hasPointerEscape(original)) {
// The move doesn't alter constraints: ownership and lexicality match.

auto *singleUser =
Copy link
Contributor

Choose a reason for hiding this comment

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

The comments are great. But I still somehow missed that that singleUser check is actually part of condition #3, and only a way to short-circuit the complete, but slower, logic below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Follow-up at #64483 .

original->getSingleUse() ? original->getSingleUse()->getUser() : nullptr;
if (mvi == singleUser && !SILArgument::asPhi(original)) {
assert(!hasPointerEscape(original));
// The moved-from value's only use is the move_value, and the moved-from
// value isn't a phi. So, it doesn't escape. (A phi's incoming values
// might escape.)
//
// Still, no optimization is enabled by separating the two lifetimes.
// The moved-from value's lifetime could not be shrunk regardless of whether
// the moved-to value escapes.
return true;
}

auto moveHasEscape = hasPointerEscape(mvi);
auto originalHasEscape = hasPointerEscape(original);

// (3) Escaping matches? (Expensive check, saved for last.)
if (moveHasEscape != originalHasEscape) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be more clear:

if (moveHasEscape == originalHasEscape)
  return true

if (!originalHasEscape) {
auto *singleConsumingUser =
original->getSingleConsumingUse()
? original->getSingleConsumingUse()->getUser()
: nullptr;
if (mvi == singleConsumingUser) {
// The moved-from value's only consuming use is the move_value and it
// doesn't escape.
//
// Although the moved-to value escapes, no optimization is enabled by
// separating the two lifetimes. The moved-from value's lifetime
// already couldn't be shrunk.
return true;
}
}
return false;
}

Expand Down
179 changes: 131 additions & 48 deletions test/SILOptimizer/semantic-arc-opts-redundant-move-elimination.sil
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,34 @@ sil @borrow : $@convention(thin) (@guaranteed C) -> ()
sil @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()

// Test that move_value instructions are removed when they are "redundant". A
// move_value instruction is redundant when the lifetime it introduces has the
// same characteristics as the lifetime that it ends along in the following two
// aspects:
// - lexicaliity
// - escapingness

// move_value instruction is redundant when the lifetime it neither
// - alters semantics (ownership, lexicality)
// - enables optimization (e.g. separating the lifetimes limits the scope
// within which values can escape and one of lifetimes can be shrunk as a
// result)
//
// For example, when the lifetime of the moved-from and the moved-to values have
// the same characteristics
// - ownership
// - lexicality
// - escaping
// then the move_value is redundant.
//
// The tests are named as follows:
//
// @test_{old_characteristics}_{new_characteristics}
// @test_{old_non_move_uses}_{old_characteristics}_{new_characteristics}
// where old_non_move_uses is
// - 0: none
// - 1: non-consuming
// - 2: consuming
// where both old_characteristics and new_characteristics are of the form
//
// {is_lexical}{has_escaping_use}
// {is_owned}{is_lexical}{has_escaping_use}
//
// and both is_lexical and has_escaping_use are 1 or 0 depending on whether each
// is true.
//
// So for example, in @test_00_10, there is a move_value instruction which ends
// So for example, in @test_1_100_110, there is a move_value instruction which ends
// a lifetime that is both neither lexical nor escaping and begins a lifetime
// which is lexical but not escaping. Since the characteristics of the old and
// new lifetimes differ, the move_value should be preserved.
Expand All @@ -43,16 +54,17 @@ sil @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()
// easier to specify the characteristics of the first lifetime. The move_value
// of real interest for the tests is the second.

// Old: lexical , non-escaping
// New: lexical , non-escaping
// Non-move use: non-consuming
// Old: owned , lexical , non-escaping
// New: owned , lexical , non-escaping
// Same. Redundant. Remove move_value.
//
// CHECK-LABEL: sil [ossa] @test_10_10 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_110_110 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: [[LIFETIME:%[^,]+]] = move_value [lexical] [[INSTANCE]]
// CHECK-NOT: move_value
// CHECK-LABEL: } // end sil function 'test_10_10'
sil [ossa] @test_10_10 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_1_110_110'
sil [ossa] @test_1_110_110 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
Expand All @@ -65,16 +77,17 @@ sil [ossa] @test_10_10 : $@convention(thin) () -> () {
return %retval : $()
}

// Old: lexical , non-escaping
// New: non-lexical, non-escaping
// Non-move use: non-consuming
// Old: owned , lexical , non-escaping
// New: owned , non-lexical, non-escaping
// Different. Non-redundant. Keep move_value.
//
// CHECK-LABEL: sil [ossa] @test_10_00 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_110_100 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: [[LIFETIME:%[^,]+]] = move_value [lexical] [[INSTANCE]]
// CHECK: move_value [[LIFETIME]]
// CHECK-LABEL: } // end sil function 'test_10_00'
sil [ossa] @test_10_00 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_1_110_100'
sil [ossa] @test_1_110_100 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
Expand All @@ -87,15 +100,16 @@ sil [ossa] @test_10_00 : $@convention(thin) () -> () {
return %retval : $()
}

// Old: non-lexical, non-escaping
// New: lexical , non-escaping
// Non-move use: non-consuming
// Old: non-owned , lexical, non-escaping
// New: lexiowned , cal , non-escaping
// Different. Non-redundant. Keep move_value.
//
// CHECK-LABEL: sil [ossa] @test_00_10 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_100_110 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: move_value [lexical] [[INSTANCE]]
// CHECK-LABEL: } // end sil function 'test_00_10'
sil [ossa] @test_00_10 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_1_100_110'
sil [ossa] @test_1_100_110 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
Expand All @@ -107,15 +121,16 @@ sil [ossa] @test_00_10 : $@convention(thin) () -> () {
return %retval : $()
}

// Old: non-lexical, non-escaping
// New: non-lexical, non-escaping
// Non-move use: non-consuming
// Old: non-owned , lexical, non-escaping
// New: non-owned , lexical, non-escaping
// Same. Redundant. Remove move_value.
//
// CHECK-LABEL: sil [ossa] @test_00_00 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_100_100 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK-NOT: move_value
// CHECK-LABEL: } // end sil function 'test_00_00'
sil [ossa] @test_00_00 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_1_100_100'
sil [ossa] @test_1_100_100 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
Expand All @@ -127,16 +142,17 @@ sil [ossa] @test_00_00 : $@convention(thin) () -> () {
return %retval : $()
}

// Old: lexical , escaping
// New: lexical , escaping
// Non-move use: non-consuming
// Old: owned , lexical , escaping
// New: owned , lexical , escaping
// Same. Redundant. Remove move_value.
//
// CHECK-LABEL: sil [ossa] @test_11_11 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_111_111 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: move_value [lexical] [[INSTANCE]]
// CHECK-NOT: move_value
// CHECK-LABEL: } // end sil function 'test_11_11'
sil [ossa] @test_11_11 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_1_111_111'
sil [ossa] @test_1_111_111 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%useUnmanaged = function_ref @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()
Expand All @@ -154,16 +170,17 @@ sil [ossa] @test_11_11 : $@convention(thin) () -> () {
return %retval : $()
}

// Old: lexical , escaping
// New: lexical , non-escaping
// Non-move use: non-consuming
// Old: owned , lexical , escaping
// New: owned , lexical , non-escaping
// Different. Non-redundant. Keep move_value.
//
// CHECK-LABEL: sil [ossa] @test_11_10 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_111_110 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: [[LIFETIME:%[^,]+]] = move_value [lexical] [[INSTANCE]]
// CHECK: move_value [lexical] [[LIFETIME]]
// CHECK-LABEL: } // end sil function 'test_11_10'
sil [ossa] @test_11_10 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_1_111_110'
sil [ossa] @test_1_111_110 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%useUnmanaged = function_ref @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()
Expand All @@ -179,22 +196,83 @@ sil [ossa] @test_11_10 : $@convention(thin) () -> () {
return %retval : $()
}

// Old: lexical , non-escaping
// New: lexical , escaping
// Different. Non-redundant. Keep move_value.
// Non-move use: non-consuming
// Old: owned , lexical , non-escaping
// New: owned , lexical , escaping
// Different, but only consuming use of original is move_value. Redundant.
// Remove move_value.
//
// CHECK-LABEL: sil [ossa] @test_10_11 : {{.*}} {
// CHECK-LABEL: sil [ossa] @test_1_110_111 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: [[LIFETIME:%[^,]+]] = move_value [lexical] [[INSTANCE]]
// CHECK-NOT: move_value
// CHECK-LABEL: } // end sil function 'test_1_110_111'
sil [ossa] @test_1_110_111 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%useUnmanaged = function_ref @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
%lifetime = move_value [lexical] %instance : $C
apply %borrow(%lifetime) : $@convention(thin) (@guaranteed C) -> ()
%lifetime2 = move_value [lexical] %lifetime : $C
%escape = ref_to_unmanaged %lifetime2 : $C to $@sil_unmanaged C
apply %useUnmanaged(%escape) : $@convention(thin) (@sil_unmanaged C) -> ()
apply %borrow(%lifetime2) : $@convention(thin) (@guaranteed C) -> ()
destroy_value %lifetime2 : $C
%retval = tuple ()
return %retval : $()
}

// Non-move use: consuming
// Old: owned , lexical , non-escaping
// New: owned , lexical , escaping
// Different, and non-move_value consuming use. Non-redundant. Keep move_value.
//
// CHECK-LABEL: sil [ossa] @test_2_110_111 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: [[LIFETIME:%[^,]+]] = move_value [lexical] [[INSTANCE]]
// CHECK: destroy_value [[LIFETIME]]
// CHECK: move_value [lexical] [[LIFETIME]]
// CHECK-LABEL: } // end sil function 'test_10_11'
sil [ossa] @test_10_11 : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_2_110_111'
sil [ossa] @test_2_110_111 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%useUnmanaged = function_ref @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
%lifetime = move_value [lexical] %instance : $C
apply %borrow(%lifetime) : $@convention(thin) (@guaranteed C) -> ()
cond_br undef, left, right
left:
destroy_value %lifetime : $C
br bottom
right:
%lifetime2 = move_value [lexical] %lifetime : $C
%escape = ref_to_unmanaged %lifetime2 : $C to $@sil_unmanaged C
apply %useUnmanaged(%escape) : $@convention(thin) (@sil_unmanaged C) -> ()
apply %borrow(%lifetime2) : $@convention(thin) (@guaranteed C) -> ()
destroy_value %lifetime2 : $C
br bottom
bottom:
%retval = tuple ()
return %retval : $()
}

// Non-move use: no
// Old: owned , lexical , non-escaping
// New: owned , lexical , escaping
// Different. Redundant. Remove move_value.
//
// CHECK-LABEL: sil [ossa] @test_0_110_111 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = apply
// CHECK: [[LIFETIME:%[^,]+]] = move_value [lexical] [[INSTANCE]]
// CHECK-NOT: move_value
// CHECK-LABEL: } // end sil function 'test_0_110_111'
sil [ossa] @test_0_110_111 : $@convention(thin) () -> () {
%getOwned = function_ref @getOwned : $@convention(thin) () -> (@owned C)
%borrow = function_ref @borrow : $@convention(thin) (@guaranteed C) -> ()
%useUnmanaged = function_ref @useUnmanaged : $@convention(thin) (@sil_unmanaged C) -> ()
%instance = apply %getOwned() : $@convention(thin) () -> (@owned C)
%lifetime = move_value [lexical] %instance : $C
%lifetime2 = move_value [lexical] %lifetime : $C
%escape = ref_to_unmanaged %lifetime2 : $C to $@sil_unmanaged C
apply %useUnmanaged(%escape) : $@convention(thin) (@sil_unmanaged C) -> ()
Expand All @@ -208,11 +286,16 @@ sil [ossa] @test_10_11 : $@convention(thin) () -> () {
// ownership, we can't determine whether the moved-from value has a pointer
// escape.
//
// CHECK-LABEL: sil [ossa] @f_none_optional : {{.*}} {
// Non-move use: no
// Old: none , non-lexical, non-escaping
// New: owned , non-lexical, escaping
// Different. Non-redundant. Keep move_value.
//
// CHECK-LABEL: sil [ossa] @test_0_000_100 : {{.*}} {
// CHECK: [[INSTANCE:%[^,]+]] = enum $FakeOptional<C>, #FakeOptional.none!enumelt
// CHECK: [[LIFETIME:%[^,]+]] = move_value [[INSTANCE]]
// CHECK-LABEL: } // end sil function 'f_none_optional'
sil [ossa] @f_none_optional : $@convention(thin) () -> () {
// CHECK-LABEL: } // end sil function 'test_0_000_100'
sil [ossa] @test_0_000_100 : $@convention(thin) () -> () {
%none = enum $FakeOptional<C>, #FakeOptional.none!enumelt
%lifetime = move_value %none : $FakeOptional<C>
destroy_value %lifetime : $FakeOptional<C>
Expand Down