Optimizer: peephole optimization for raw-value enum comparsions

Optimize (the very inefficient) RawRepresentable comparison function call to a simple compare of enum tags.
For example,
```
  enum E: String {
    case  a, b, c
  }
```
is compared by getting the raw values of both operands and doing a string compare.
This peephole optimizations replaces the call to such a comparison function with a direct compare of the enum tags, which boils down to a single integer comparison instruction.

rdar://151788987
This commit is contained in:
Erik Eckstein
2025-05-26 18:34:39 +02:00
parent 0486f1a7f7
commit cac594fb86
6 changed files with 124 additions and 1 deletions

View File

@@ -28,6 +28,9 @@ extension ApplyInst : OnoneSimplifiable, SILCombineSimplifiable {
if tryRemoveArrayCast(apply: self, context) {
return
}
if tryOptimizeEnumComparison(apply: self, context) {
return
}
if !context.preserveDebugInfo {
_ = tryReplaceExistentialArchetype(of: self, context)
}
@@ -110,6 +113,48 @@ private func tryRemoveArrayCast(apply: ApplyInst, _ context: SimplifyContext) ->
return true
}
/// Optimize (the very inefficient) RawRepresentable comparison to a simple compare of enum tags.
/// For example,
/// ```
/// enum E: String {
/// case a, b, c
/// }
/// ```
/// is compared by getting the raw values of both operands and doing a string compare.
/// This peephole optimizations replaces the call to such a comparison function with a direct compare of
/// the enum tags, which boils down to a single integer comparison instruction.
///
private func tryOptimizeEnumComparison(apply: ApplyInst, _ context: SimplifyContext) -> Bool {
guard let callee = apply.referencedFunction,
apply.arguments.count == 2,
callee.hasSemanticsAttribute("rawrepresentable.is_equal"),
apply.type.isStruct
else {
return false
}
let lhs = apply.arguments[0]
let rhs = apply.arguments[1]
guard let enumDecl = lhs.type.nominal as? EnumDecl,
!enumDecl.isResilient(in: apply.parentFunction),
!enumDecl.hasClangNode,
lhs.type.isAddress,
lhs.type == rhs.type
else {
return false
}
let builder = Builder(before: apply, context)
let tagType = context.getBuiltinIntegerType(bitWidth: 32)
let lhsTag = builder.createBuiltin(name: "getEnumTag", type: tagType,
substitutions: apply.substitutionMap, arguments: [lhs])
let rhsTag = builder.createBuiltin(name: "getEnumTag", type: tagType,
substitutions: apply.substitutionMap, arguments: [rhs])
let builtinBoolType = context.getBuiltinIntegerType(bitWidth: 1)
let cmp = builder.createBuiltin(name: "cmp_eq_Int32", type: builtinBoolType, arguments: [lhsTag, rhsTag])
let booleanResult = builder.createStruct(type: apply.type, elements: [cmp])
apply.replace(with: booleanResult, context)
return true
}
/// If the apply uses an existential archetype (`@opened("...")`) and the concrete type is known,
/// replace the existential archetype with the concrete type
/// 1. in the apply's substitution map

View File

@@ -149,6 +149,7 @@ public protocol RawRepresentable<RawValue> {
/// - lhs: A raw-representable instance.
/// - rhs: A second raw-representable instance.
@inlinable // trivial-implementation
@_semantics("rawrepresentable.is_equal")
public func == <T: RawRepresentable>(lhs: T, rhs: T) -> Bool
where T.RawValue: Equatable {
return lhs.rawValue == rhs.rawValue

View File

@@ -64,7 +64,7 @@ func test6() {
// CHECK: function_ref @$[[TEST6_EQUALS_WITNESS:[_0-9a-zA-Z]+]]
// CHECK: }
// CHECK: sil [serialized] @$[[TEST6_EQUALS_WITNESS]] : $@convention(thin) <τ_0_0 where τ_0_0 : RawRepresentable, τ_0_0.RawValue : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool
// CHECK: sil [serialized] {{.*}}@$[[TEST6_EQUALS_WITNESS]] : $@convention(thin) <τ_0_0 where τ_0_0 : RawRepresentable, τ_0_0.RawValue : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool
func test7() {
struct Outer {

View File

@@ -0,0 +1,41 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift -O -module-name=test %s -o %t/a.out
// RUN: %target-build-swift -O -module-name=test %s -emit-ir | %FileCheck %s
// RUN: %target-codesign %t/a.out
// RUN: %target-run %t/a.out | %FileCheck %s --check-prefix=OUT
// REQUIRES: executable_test,optimized_stdlib
enum E: String {
case a, b, c, long_case_name_for_testing, d, e
}
// CHECK-LABEL: define {{.*}} i1 @"$s4test9compareeqySbAA1EO_ADtF"(i8 %0, i8 %1)
// CHECK: %2 = icmp eq i8 %0, %1
// CHECK-NEXT: ret i1 %2
@inline(never)
func compareeq(_ a: E, _ b: E) -> Bool {
return a == b
}
// CHECK-LABEL: define {{.*}} i1 @"$s4test9compareneySbAA1EO_ADtF"(i8 %0, i8 %1)
// CHECK: %2 = icmp ne i8 %0, %1
// CHECK-NEXT: ret i1 %2
@inline(never)
func comparene(_ a: E, _ b: E) -> Bool {
return a != b
}
// OUT: 1: false
print("1: \(compareeq(.c, .long_case_name_for_testing))")
// OUT: 2: true
print("2: \(compareeq(.c, .c))")
// OUT: 3: true
print("3: \(comparene(.c, .long_case_name_for_testing))")
// OUT: 4: false
print("4: \(comparene(.c, .c))")

View File

@@ -26,6 +26,10 @@ struct GenS<T> {
var x: Int
}
enum E: String {
case a, b, c, d, e
}
sil @cl : $@convention(thin) () -> Int
// CHECK-LABEL: sil [ossa] @thick_to_thin :
@@ -165,3 +169,31 @@ bb0(%0 : @guaranteed $Array<Any>):
return %2
}
sil [_semantics "rawrepresentable.is_equal"] @rawrepresentable_is_equal : $@convention(thin) <T where T : RawRepresentable, T.RawValue : Equatable> (@in_guaranteed T, @in_guaranteed T) -> Bool
sil [_semantics "rawrepresentable.is_equal"] @rawrepresentable_is_equal_wrong_convention : $@convention(thin) (E, E) -> Bool
// CHECK-LABEL: sil [ossa] @string_enum_is_equal :
// CHECK: %2 = builtin "getEnumTag"<E>(%0) : $Builtin.Int32
// CHECK: %3 = builtin "getEnumTag"<E>(%1) : $Builtin.Int32
// CHECK: %4 = builtin "cmp_eq_Int32"(%2, %3) : $Builtin.Int1
// CHECK: %5 = struct $Bool (%4)
// CHECK: return %5
// CHECK: } // end sil function 'string_enum_is_equal'
sil [ossa] @string_enum_is_equal : $@convention(thin) (@in_guaranteed E, @in_guaranteed E) -> Bool {
bb0(%0 : $*E, %1 : $*E):
%2 = function_ref @rawrepresentable_is_equal : $@convention(thin) <T where T : RawRepresentable, T.RawValue : Equatable> (@in_guaranteed T, @in_guaranteed T) -> Bool
%3 = apply %2<E>(%0, %1) : $@convention(thin) <τ_0_0 where τ_0_0 : RawRepresentable, τ_0_0.RawValue : Equatable> (@in_guaranteed τ_0_0, @in_guaranteed τ_0_0) -> Bool
return %3
}
// CHECK-LABEL: sil [ossa] @string_enum_is_equal_wrong_convention :
// CHECK: function_ref
// CHECK: apply
// CHECK: } // end sil function 'string_enum_is_equal_wrong_convention'
sil [ossa] @string_enum_is_equal_wrong_convention : $@convention(thin) (E, E) -> Bool {
bb0(%0 : $E, %1 : $E):
%2 = function_ref @rawrepresentable_is_equal_wrong_convention : $@convention(thin) (E, E) -> Bool
%3 = apply %2(%0, %1) : $@convention(thin) (E, E) -> Bool
return %3
}

View File

@@ -857,6 +857,10 @@ Var UnsafeMutableBufferPointer.indices has mangled name changing from 'Swift.Uns
Var UnsafeMutableBufferPointer.indices is now with @_preInverseGenerics
Func !=(_:_:) has been removed
Func ==(_:_:) has been removed
Func ==(_:_:) has generic signature change from to <T where T : Swift.RawRepresentable, T.RawValue : Swift.Equatable>
Func ==(_:_:) has mangled name changing from 'Swift.== infix(Swift.Optional<Any.Type>, Swift.Optional<Any.Type>) -> Swift.Bool' to 'Swift.== infix<A where A: Swift.RawRepresentable, A.RawValue: Swift.Equatable>(A, A) -> Swift.Bool'
Func ==(_:_:) has parameter 0 type change from (any Any.Type)? to τ_0_0
Func ==(_:_:) has parameter 1 type change from (any Any.Type)? to τ_0_0
Func type(of:) has been removed
// *** DO NOT DISABLE OR XFAIL THIS TEST. *** (See comment above.)