Skip to content

Commit 89bdb31

Browse files
authored
Merge pull request #81780 from eeckstein/optimize-enum-comparison
Optimize enum comparisons
2 parents dd59797 + ed8922b commit 89bdb31

File tree

12 files changed

+230
-4
lines changed

12 files changed

+230
-4
lines changed

SwiftCompilerSources/Sources/Basic/Utils.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,15 @@ public extension NoReflectionChildren {
7979
// StringRef
8080
//===----------------------------------------------------------------------===//
8181

82-
public struct StringRef : CustomStringConvertible, NoReflectionChildren {
82+
public struct StringRef : CustomStringConvertible, NoReflectionChildren, ExpressibleByStringLiteral {
8383
public let _bridged: BridgedStringRef
8484

8585
public init(bridged: BridgedStringRef) { self._bridged = bridged }
8686

87+
public init(stringLiteral: StaticString) {
88+
self._bridged = BridgedStringRef(data: stringLiteral.utf8Start, count: stringLiteral.utf8CodeUnitCount)
89+
}
90+
8791
public var string: String { String(_bridged) }
8892
public var description: String { string }
8993

SwiftCompilerSources/Sources/Optimizer/InstructionSimplification/SimplifyApply.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ extension ApplyInst : OnoneSimplifiable, SILCombineSimplifiable {
2828
if tryRemoveArrayCast(apply: self, context) {
2929
return
3030
}
31+
if tryOptimizeEnumComparison(apply: self, context) {
32+
return
33+
}
3134
if !context.preserveDebugInfo {
3235
_ = tryReplaceExistentialArchetype(of: self, context)
3336
}
@@ -110,6 +113,48 @@ private func tryRemoveArrayCast(apply: ApplyInst, _ context: SimplifyContext) ->
110113
return true
111114
}
112115

116+
/// Optimize (the very inefficient) RawRepresentable comparison to a simple compare of enum tags.
117+
/// For example,
118+
/// ```
119+
/// enum E: String {
120+
/// case a, b, c
121+
/// }
122+
/// ```
123+
/// is compared by getting the raw values of both operands and doing a string compare.
124+
/// This peephole optimizations replaces the call to such a comparison function with a direct compare of
125+
/// the enum tags, which boils down to a single integer comparison instruction.
126+
///
127+
private func tryOptimizeEnumComparison(apply: ApplyInst, _ context: SimplifyContext) -> Bool {
128+
guard let callee = apply.referencedFunction,
129+
apply.arguments.count == 2,
130+
callee.hasSemanticsAttribute("rawrepresentable.is_equal"),
131+
apply.type.isStruct
132+
else {
133+
return false
134+
}
135+
let lhs = apply.arguments[0]
136+
let rhs = apply.arguments[1]
137+
guard let enumDecl = lhs.type.nominal as? EnumDecl,
138+
!enumDecl.isResilient(in: apply.parentFunction),
139+
!enumDecl.hasClangNode,
140+
lhs.type.isAddress,
141+
lhs.type == rhs.type
142+
else {
143+
return false
144+
}
145+
let builder = Builder(before: apply, context)
146+
let tagType = context.getBuiltinIntegerType(bitWidth: 32)
147+
let lhsTag = builder.createBuiltin(name: "getEnumTag", type: tagType,
148+
substitutions: apply.substitutionMap, arguments: [lhs])
149+
let rhsTag = builder.createBuiltin(name: "getEnumTag", type: tagType,
150+
substitutions: apply.substitutionMap, arguments: [rhs])
151+
let builtinBoolType = context.getBuiltinIntegerType(bitWidth: 1)
152+
let cmp = builder.createBuiltin(name: "cmp_eq_Int32", type: builtinBoolType, arguments: [lhsTag, rhsTag])
153+
let booleanResult = builder.createStruct(type: apply.type, elements: [cmp])
154+
apply.replace(with: booleanResult, context)
155+
return true
156+
}
157+
113158
/// If the apply uses an existential archetype (`@opened("...")`) and the concrete type is known,
114159
/// replace the existential archetype with the concrete type
115160
/// 1. in the apply's substitution map

include/swift/AST/SemanticAttrs.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ SEMANTICS_ATTR(OPTIMIZE_SIL_SPECIALIZE_GENERIC_PARTIAL_NEVER,
7373
"optimize.sil.specialize.generic.partial.never")
7474
SEMANTICS_ATTR(OPTIMIZE_SIL_INLINE_CONSTANT_ARGUMENTS,
7575
"optimize.sil.inline.constant.arguments")
76+
SEMANTICS_ATTR(DERIVED_ENUM_EQUALS,
77+
"derived_enum_equals")
7678
SEMANTICS_ATTR(OPTIMIZE_SIL_SPECIALIZE_GENERIC_SIZE_NEVER,
7779
"optimize.sil.specialize.generic.size.never")
7880
SEMANTICS_ATTR(OPTIMIZE_SIL_SPECIALIZE_OWNED2GUARANTEE_NEVER,

lib/SILOptimizer/Transforms/PerformanceInliner.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,14 @@ static bool hasConstantArguments(FullApplySite fas) {
524524
return true;
525525
}
526526

527+
static bool hasConstantEnumArgument(FullApplySite fas) {
528+
for (SILValue arg : fas.getArguments()) {
529+
if (isa<EnumInst>(arg))
530+
return true;
531+
}
532+
return false;
533+
}
534+
527535
bool SILPerformanceInliner::isProfitableToInline(
528536
FullApplySite AI, Weight CallerWeight, ConstantTracker &callerTracker,
529537
int &NumCallerBlocks,
@@ -597,6 +605,13 @@ bool SILPerformanceInliner::isProfitableToInline(
597605
return true;
598606
}
599607

608+
// If there is a "constant" enum argument to a synthesized enum comparison,
609+
// we can always inline it, because most of it will be constant folded anyway.
610+
if (Callee->hasSemanticsAttr(semantics::DERIVED_ENUM_EQUALS) &&
611+
hasConstantEnumArgument(AI)) {
612+
return true;
613+
}
614+
600615
// Bail out if this generic call can be optimized by means of
601616
// the generic specialization, because we prefer generic specialization
602617
// to inlining of generics.

lib/Sema/DerivedConformance/DerivedConformanceEquatableHashable.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,12 @@ deriveEquatable_eq(
394394
auto boolTy = C.getBoolType();
395395

396396
Identifier generatedIdentifier;
397+
bool isDerivedEnumEquals = false;
397398
if (parentDC->getParentModule()->isResilient()) {
398399
generatedIdentifier = C.Id_EqualsOperator;
399400
} else if (selfIfaceTy->getEnumOrBoundGenericEnum()) {
400401
generatedIdentifier = C.Id_derived_enum_equals;
402+
isDerivedEnumEquals = true;
401403
} else {
402404
assert(selfIfaceTy->getStructOrBoundGenericStruct());
403405
generatedIdentifier = C.Id_derived_struct_equals;
@@ -411,6 +413,9 @@ deriveEquatable_eq(
411413
/*GenericParams=*/nullptr, params, boolTy, parentDC);
412414
eqDecl->setUserAccessible(false);
413415
eqDecl->setSynthesized();
416+
if (isDerivedEnumEquals) {
417+
eqDecl->getAttrs().add(new (C) SemanticsAttr("derived_enum_equals", SourceLoc(), SourceRange(), /*Implicit=*/true));
418+
}
414419

415420
// Add the @_implements(Equatable, ==(_:_:)) attribute
416421
if (generatedIdentifier != C.Id_EqualsOperator) {

stdlib/public/core/CompilerProtocols.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public protocol RawRepresentable<RawValue> {
149149
/// - lhs: A raw-representable instance.
150150
/// - rhs: A second raw-representable instance.
151151
@inlinable // trivial-implementation
152+
@_semantics("rawrepresentable.is_equal")
152153
public func == <T: RawRepresentable>(lhs: T, rhs: T) -> Bool
153154
where T.RawValue: Equatable {
154155
return lhs.rawValue == rhs.rawValue

test/SILGen/protocol_operators_local_conformance.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func test6() {
6464
// CHECK: function_ref @$[[TEST6_EQUALS_WITNESS:[_0-9a-zA-Z]+]]
6565
// CHECK: }
6666

67-
// 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
67+
// 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
6868

6969
func test7() {
7070
struct Outer {

test/SILGen/synthesized_conformance_class.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class Nonfinal<T> {
5656

5757
// CHECK-LABEL: sil private [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}11stringValueAFyx_GSgSS_tcfC : $@convention(method) <T> (@owned String, @thin Final<T>.CodingKeys.Type) -> Optional<Final<T>.CodingKeys> {
5858
// CHECK-LABEL: sil private [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}8intValueAFyx_GSgSi_tcfC : $@convention(method) <T> (Int, @thin Final<T>.CodingKeys.Type) -> Optional<Final<T>.CodingKeys> {
59-
// CHECK-LABEL: sil private [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}21__derived_enum_equalsySbAFyx_G_AHtFZ : $@convention(method) <T> (Final<T>.CodingKeys, Final<T>.CodingKeys, @thin Final<T>.CodingKeys.Type) -> Bool {
59+
// CHECK-LABEL: sil private [_semantics "derived_enum_equals"] [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}21__derived_enum_equalsySbAFyx_G_AHtFZ : $@convention(method) <T> (Final<T>.CodingKeys, Final<T>.CodingKeys, @thin Final<T>.CodingKeys.Type) -> Bool {
6060
// CHECK-LABEL: sil private [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}4hash4intoys6HasherVz_tF : $@convention(method) <T> (@inout Hasher, Final<T>.CodingKeys) -> () {
6161
// CHECK-LABEL: sil private [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}9hashValueSivg : $@convention(method) <T> (Final<T>.CodingKeys) -> Int {
6262
// CHECK-LABEL: sil private [ossa] @$s29synthesized_conformance_class5FinalC10CodingKeys{{.*}}8intValueSiSgvg : $@convention(method) <T> (Final<T>.CodingKeys) -> Optional<Int> {

test/SILGen/synthesized_conformance_enum.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ enum NoValues {
3737
extension Enum: Equatable where T: Equatable {}
3838
// CHECK-FRAGILE-LABEL: // static Enum<A>.__derived_enum_equals(_:_:)
3939
// CHECK-FRAGILE-NEXT: // Isolation: unspecified
40-
// CHECK-FRAGILE-NEXT: sil hidden [ossa] @$s28synthesized_conformance_enum4EnumOAASQRzlE010__derived_C7_equalsySbACyxG_AEtFZ : $@convention(method) <T where T : Equatable> (@in_guaranteed Enum<T>, @in_guaranteed Enum<T>, @thin Enum<T>.Type) -> Bool {
40+
// CHECK-FRAGILE-NEXT: sil hidden [_semantics "derived_enum_equals"] [ossa] @$s28synthesized_conformance_enum4EnumOAASQRzlE010__derived_C7_equalsySbACyxG_AEtFZ : $@convention(method) <T where T : Equatable> (@in_guaranteed Enum<T>, @in_guaranteed Enum<T>, @thin Enum<T>.Type) -> Bool {
4141
// CHECK-RESILIENT-LABEL: // static Enum<A>.== infix(_:_:)
4242
// CHECK-RESILIENT-NEXT: // Isolation: unspecified
4343
// CHECK-RESILIENT-NEXT: sil hidden [ossa] @$s28synthesized_conformance_enum4EnumOAASQRzlE2eeoiySbACyxG_AEtFZ : $@convention(method) <T where T : Equatable> (@in_guaranteed Enum<T>, @in_guaranteed Enum<T>, @thin Enum<T>.Type) -> Bool {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -O -module-name=test %s -o %t/a.out
3+
// RUN: %target-build-swift -O -module-name=test %s -emit-ir | %FileCheck %s
4+
// RUN: %target-codesign %t/a.out
5+
// RUN: %target-run %t/a.out | %FileCheck %s --check-prefix=OUT
6+
7+
// REQUIRES: executable_test,optimized_stdlib
8+
9+
enum E: String {
10+
case a, b, c, long_case_name_for_testing, d, e
11+
}
12+
13+
// CHECK-LABEL: define {{.*}} i1 @"$s4test9compareeqySbAA1EO_ADtF"(i8 %0, i8 %1)
14+
// CHECK: %2 = icmp eq i8 %0, %1
15+
// CHECK-NEXT: ret i1 %2
16+
@inline(never)
17+
func compareeq(_ a: E, _ b: E) -> Bool {
18+
return a == b
19+
}
20+
21+
// CHECK-LABEL: define {{.*}} i1 @"$s4test9compareneySbAA1EO_ADtF"(i8 %0, i8 %1)
22+
// CHECK: %2 = icmp ne i8 %0, %1
23+
// CHECK-NEXT: ret i1 %2
24+
@inline(never)
25+
func comparene(_ a: E, _ b: E) -> Bool {
26+
return a != b
27+
}
28+
29+
enum LargeEnum: Equatable {
30+
case a1, a2, a3, a4, a5, a6, a7, a8, a9
31+
case b1, b2, b3, b4, b5, b6, b7, b8, b9
32+
case c1, c2, c3, c4, c5, c6, c7, c8, c9
33+
case d1, d2, d3, d4, d5, d6, d7, d8, d9
34+
case e1(Int64), e2(Int64), e3(Int64), e4(Int64), e5(Int64), e6(Int64), e7(Int64), e8(Int64), e9(Int64)
35+
case f1, f2, f3, f4, f5, f6, f7, f8, f9
36+
case g1, g2, g3, g4, g5, g6, g7, g8, g9
37+
}
38+
39+
// CHECK-LABEL: define {{.*}} i1 @"$s4test8compare1ySbAA9LargeEnumOF"(i64 %0, i8 %1)
40+
// CHECK: entry:
41+
// CHECK-NEXT: icmp
42+
// CHECK-NEXT: icmp
43+
// CHECK-NEXT: {{(and|select)}}
44+
// CHECK-NEXT: ret
45+
@inline(never)
46+
func compare1(_ x: LargeEnum) -> Bool {
47+
return x == .b2
48+
}
49+
50+
// CHECK-LABEL: define {{.*}} i1 @"$s4test8compare2ySbAA9LargeEnumOF"(i64 %0, i8 %1)
51+
// CHECK: entry:
52+
// CHECK-NEXT: icmp
53+
// CHECK-NEXT: icmp
54+
// CHECK-NEXT: {{(and|select)}}
55+
// CHECK-NEXT: ret
56+
@inline(never)
57+
func compare2(_ x: LargeEnum) -> Bool {
58+
return .f2 == x
59+
}
60+
61+
// CHECK-LABEL: define {{.*}} i1 @"$s4test8compare3ySbAA9LargeEnumOF"(i64 %0, i8 %1)
62+
// CHECK: entry:
63+
// CHECK-NEXT: icmp
64+
// CHECK-NEXT: icmp
65+
// CHECK-NEXT: {{(and|select)}}
66+
// CHECK-NEXT: ret
67+
@inline(never)
68+
func compare3(_ x: LargeEnum) -> Bool {
69+
return .e2(27) == x
70+
}
71+
72+
// CHECK-LABEL: define {{.*}} i1 @"$s4test8compare4ySbAA9LargeEnumOF"(i64 %0, i8 %1)
73+
// CHECK: entry:
74+
// CHECK-NEXT: icmp
75+
// CHECK-NEXT: icmp
76+
// CHECK-NEXT: {{(and|select)}}
77+
// CHECK-NEXT: ret
78+
@inline(never)
79+
func compare4(_ x: LargeEnum) -> Bool {
80+
return x == .e3(28)
81+
}
82+
83+
// OUT: 1: false
84+
print("1: \(compareeq(.c, .long_case_name_for_testing))")
85+
86+
// OUT: 2: true
87+
print("2: \(compareeq(.c, .c))")
88+
89+
// OUT: 3: true
90+
print("3: \(comparene(.c, .long_case_name_for_testing))")
91+
92+
// OUT: 4: false
93+
print("4: \(comparene(.c, .c))")
94+
95+
// OUT: 5: false
96+
print("5: \(compare1(.b1))")
97+
98+
// OUT: 6: true
99+
print("6: \(compare1(.b2))")
100+
101+
// OUT: 7: false
102+
print("7: \(compare2(.b1))")
103+
104+
// OUT: 8: true
105+
print("8: \(compare2(.f2))")
106+
107+
// OUT: 9: true
108+
print("9: \(compare3(.e2(27)))")
109+
110+
// OUT: 10: false
111+
print("10: \(compare3(.e2(28)))")
112+
113+
// OUT: 11: true
114+
print("11: \(compare4(.e3(28)))")
115+
116+
// OUT: 12: false
117+
print("12: \(compare4(.e3(27)))")
118+

test/SILOptimizer/simplify_apply.sil

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ struct GenS<T> {
2626
var x: Int
2727
}
2828

29+
enum E: String {
30+
case a, b, c, d, e
31+
}
32+
2933
sil @cl : $@convention(thin) () -> Int
3034

3135
// CHECK-LABEL: sil [ossa] @thick_to_thin :
@@ -165,3 +169,31 @@ bb0(%0 : @guaranteed $Array<Any>):
165169
return %2
166170
}
167171

172+
sil [_semantics "rawrepresentable.is_equal"] @rawrepresentable_is_equal : $@convention(thin) <T where T : RawRepresentable, T.RawValue : Equatable> (@in_guaranteed T, @in_guaranteed T) -> Bool
173+
sil [_semantics "rawrepresentable.is_equal"] @rawrepresentable_is_equal_wrong_convention : $@convention(thin) (E, E) -> Bool
174+
175+
// CHECK-LABEL: sil [ossa] @string_enum_is_equal :
176+
// CHECK: %2 = builtin "getEnumTag"<E>(%0) : $Builtin.Int32
177+
// CHECK: %3 = builtin "getEnumTag"<E>(%1) : $Builtin.Int32
178+
// CHECK: %4 = builtin "cmp_eq_Int32"(%2, %3) : $Builtin.Int1
179+
// CHECK: %5 = struct $Bool (%4)
180+
// CHECK: return %5
181+
// CHECK: } // end sil function 'string_enum_is_equal'
182+
sil [ossa] @string_enum_is_equal : $@convention(thin) (@in_guaranteed E, @in_guaranteed E) -> Bool {
183+
bb0(%0 : $*E, %1 : $*E):
184+
%2 = function_ref @rawrepresentable_is_equal : $@convention(thin) <T where T : RawRepresentable, T.RawValue : Equatable> (@in_guaranteed T, @in_guaranteed T) -> Bool
185+
%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
186+
return %3
187+
}
188+
189+
// CHECK-LABEL: sil [ossa] @string_enum_is_equal_wrong_convention :
190+
// CHECK: function_ref
191+
// CHECK: apply
192+
// CHECK: } // end sil function 'string_enum_is_equal_wrong_convention'
193+
sil [ossa] @string_enum_is_equal_wrong_convention : $@convention(thin) (E, E) -> Bool {
194+
bb0(%0 : $E, %1 : $E):
195+
%2 = function_ref @rawrepresentable_is_equal_wrong_convention : $@convention(thin) (E, E) -> Bool
196+
%3 = apply %2(%0, %1) : $@convention(thin) (E, E) -> Bool
197+
return %3
198+
}
199+

test/api-digester/stability-stdlib-abi-without-asserts.test

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,10 @@ Var UnsafeMutableBufferPointer.indices has mangled name changing from 'Swift.Uns
857857
Var UnsafeMutableBufferPointer.indices is now with @_preInverseGenerics
858858
Func !=(_:_:) has been removed
859859
Func ==(_:_:) has been removed
860+
Func ==(_:_:) has generic signature change from to <T where T : Swift.RawRepresentable, T.RawValue : Swift.Equatable>
861+
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'
862+
Func ==(_:_:) has parameter 0 type change from (any Any.Type)? to τ_0_0
863+
Func ==(_:_:) has parameter 1 type change from (any Any.Type)? to τ_0_0
860864
Func type(of:) has been removed
861865

862866
// *** DO NOT DISABLE OR XFAIL THIS TEST. *** (See comment above.)

0 commit comments

Comments
 (0)