[Pitch] Swift Predicates

Just one quick feedback before we try to dig further down into it, but initial samples shows that the new Predicates are around 10x slower than our current implementation for fairly simple use cases, it seems key path usage is a major factor here (see two instances of swift_getAtKeyPath in the sample below highlighted with ***** consuming the majority of the time):

2.99 Gc  100.0%	-	 	PredicateBenchmark (91361)
2.99 Gc  100.0%	-	 	 completeTaskAndRelease(swift::AsyncContext*, swift::SwiftError*)
2.99 Gc  100.0%	-	 	  specialized thunk for @escaping @convention(thin) @async () -> ()
2.99 Gc  100.0%	-	 	   static BenchmarkRunnerHooks.main()
2.99 Gc  100.0%	-	 	    BenchmarkRunner.run()
2.99 Gc  100.0%	-	 	     BenchmarkExecutor.run(_:)
2.98 Gc   99.7%	-	 	      Benchmark.run()
2.98 Gc   99.7%	-	 	       closure #1 in Benchmark.init(_:configuration:closure:setup:teardown:)
2.98 Gc   99.7%	-	 	        partial apply for closure #4 in closure #1 in variable initialization expression of benchmarks
2.97 Gc   99.3%	-	 	         closure #4 in closure #1 in variable initialization expression of benchmarks
2.86 Gc   95.6%	-	 	          Predicate.evaluate(_:)
2.33 Gc   77.8%	-	 	           protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Disjunction<A, B>
2.32 Gc   77.6%	-	 	            PredicateExpressions.Disjunction.evaluate(_:)
1.18 Gc   39.6%	-	 	             protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Disjunction<A, B>
1.18 Gc   39.4%	-	 	              PredicateExpressions.Disjunction.evaluate(_:)
1.16 Gc   38.8%	-	 	               protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Equal<A, B>
1.15 Gc   38.3%	-	 	                PredicateExpressions.Equal.evaluate(_:)
1.08 Gc   36.2%	-	 	                 protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.KeyPath<A, B>
1.08 Gc   35.9%	-	 	                  PredicateExpressions.KeyPath.evaluate(_:)
***** 803.31 Mc   26.8%	-	 	                   swift_getAtKeyPath
214.28 Mc    7.1%	-	 	                   protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Variable<A>
41.86 Mc    1.3%	-	 	                   destroy for Transaction
6.00 Mc    0.2%	6.00 Mc	 	                   PredicateExpressions.KeyPath.evaluate(_:)
3.75 Mc    0.1%	-	 	                   swift_bridgeObjectRelease
3.00 Mc    0.1%	-	 	                   PredicateExpressions.Variable.evaluate(_:)
2.00 Mc    0.0%	-	 	                   KeyPath._projectReadOnly(from:)
1.00 Mc    0.0%	-	 	                   swift_release
6.00 Mc    0.2%	-	 	                  ___chkstk_darwin
1.00 Mc    0.0%	-	 	                  swift_getAtKeyPath
1.00 Mc    0.0%	-	 	                  destroy for Transaction
33.67 Mc    1.1%	-	 	                 protocol witness for static Equatable.== infix(_:_:) in conformance <A> A?
9.00 Mc    0.3%	-	 	                 swift::metadataimpl::ValueWitnesses<swift::metadataimpl::NativeBox<unsigned long long, 8ul, 8ul, 8ul>>::initializeWithCopy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*)
7.00 Mc    0.2%	-	 	                 _platform_memmove
5.98 Mc    0.2%	5.98 Mc	 	                 PredicateExpressions.Equal.evaluate(_:)
3.00 Mc    0.1%	-	 	                 DYLD-STUB$$memcpy
2.00 Mc    0.0%	-	 	                 swift_getAssociatedTypeWitness
1.00 Mc    0.0%	-	 	                 PredicateExpressions.Value.evaluate(_:)
1.00 Mc    0.0%	-	 	                 PredicateExpressions.KeyPath.evaluate(_:)
6.00 Mc    0.2%	-	 	                ___chkstk_darwin
3.00 Mc    0.1%	-	 	                swift::metadataimpl::ValueWitnesses<swift::metadataimpl::NativeBox<unsigned long long, 8ul, 8ul, 8ul>>::destroy(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*)
2.00 Mc    0.0%	-	 	                protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Value<A>
2.00 Mc    0.0%	-	 	                protocol witness for static Equatable.== infix(_:_:) in conformance <A> A?
1.00 Mc    0.0%	-	 	                DYLD-STUB$$swift_getAssociatedTypeWitness
1.00 Mc    0.0%	-	 	                protocol witness for static Equatable.== infix(_:_:) in conformance AutoreleasingUnsafeMutablePointer<A>
11.31 Mc    0.3%	-	 	               initializeWithCopy for PredicateExpressions.Disjunction
3.28 Mc    0.1%	-	 	               destroy for PredicateExpressions.Disjunction
3.02 Mc    0.1%	-	 	               destroy for PredicateExpressions.Equal
1.00 Mc    0.0%	-	 	               PredicateExpressions.Equal.evaluate(_:)
1.00 Mc    0.0%	1.00 Mc	 	                PredicateExpressions.Equal.evaluate(_:)
1.00 Mc    0.0%	1.00 Mc	 	               PredicateExpressions.Disjunction.evaluate(_:)
3.00 Mc    0.1%	-	 	              destroy for PredicateExpressions.Equal
1.00 Mc    0.0%	-	 	              ___chkstk_darwin
1.05 Gc   35.2%	-	 	             protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Conjunction<A, B>
1.05 Gc   34.9%	-	 	              PredicateExpressions.Conjunction.evaluate(_:)
835.15 Mc   27.9%	-	 	               protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Equal<A, B>
829.55 Mc   27.7%	-	 	                PredicateExpressions.Equal.evaluate(_:)
760.72 Mc   25.4%	-	 	                 protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.KeyPath<A, B>
754.72 Mc   25.2%	-	 	                  PredicateExpressions.KeyPath.evaluate(_:)
***** 548.11 Mc   18.3%	-	 	                   swift_getAtKeyPath
180.71 Mc    6.0%	-	 	                   protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Variable<A>
18.23 Mc    0.6%	-	 	                   destroy for Transaction
3.67 Mc    0.1%	-	 	                   swift_release
2.00 Mc    0.0%	-	 	                   swift_getAssociatedTypeWitness
1.00 Mc    0.0%	-	 	                   KeyPath._projectReadOnly(from:)
1.00 Mc    0.0%	-	 	                   PredicateExpressions.Variable.evaluate(_:)
1.00 Mc    0.0%	-	 	                  destroy for Transaction
1.00 Mc    0.0%	-	 	                  ___chkstk_darwin
1.00 Mc    0.0%	-	 	                  DYLD-STUB$$swift_getAtKeyPath
1.00 Mc    0.0%	-	 	                  swift_getAtKeyPath
1.00 Mc    0.0%	1.00 Mc	 	                  protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.KeyPath<A, B>
1.00 Mc    0.0%	-	 	                  destroy for Transaction
54.83 Mc    1.8%	-	 	                 protocol witness for static Equatable.== infix(_:_:) in conformance <A> A?
3.00 Mc    0.1%	-	 	                 DYLD-STUB$$memcpy
3.00 Mc    0.1%	-	 	                 _platform_memmove
2.00 Mc    0.0%	-	 	                 swift_getAssociatedTypeWitness
2.00 Mc    0.0%	-	 	                 swift::metadataimpl::ValueWitnesses<swift::metadataimpl::NativeBox<unsigned long long, 8ul, 8ul, 8ul>>::destroy(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*)
1.00 Mc    0.0%	-	 	                 protocol witness for static Equatable.== infix(_:_:) in conformance AutoreleasingUnsafeMutablePointer<A>
1.00 Mc    0.0%	-	 	                 pod_copy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*)
1.00 Mc    0.0%	-	 	                 swift::metadataimpl::FixedSizeBufferValueWitnesses<swift::metadataimpl::ValueWitnesses<swift::metadataimpl::NativeBox<unsigned long long, 8ul, 8ul, 8ul>>, true, 8ul, 8ul, false>::getEnumTagSinglePayload(swift::OpaqueValue const*, unsigned int, swift::TargetMetadata<swift::InProcess> const*)
1.00 Mc    0.0%	1.00 Mc	 	                 PredicateExpressions.Equal.evaluate(_:)
2.00 Mc    0.0%	-	 	                ___chkstk_darwin
2.00 Mc    0.0%	-	 	                protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Value<A>
1.00 Mc    0.0%	-	 	                pod_destroy(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*)
596.61 Kc    0.0%	-	 	                DYLD-STUB$$swift_getAssociatedTypeWitness
152.54 Mc    5.1%	-	 	               protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Disjunction<A, B>
26.74 Mc    0.8%	-	 	               initializeWithCopy for PredicateExpressions.Conjunction
11.00 Mc    0.3%	-	 	               destroy for PredicateExpressions.Equal
7.00 Mc    0.2%	-	 	               destroy for PredicateExpressions.Conjunction
5.00 Mc    0.1%	-	 	               destroy for PredicateExpressions.Disjunction
4.00 Mc    0.1%	4.00 Mc	 	               PredicateExpressions.Conjunction.evaluate(_:)
2.00 Mc    0.0%	-	 	               pod_destroy(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*)
1.00 Mc    0.0%	-	 	               destroy for PredicateExpressions.KeyPath
1.00 Mc    0.0%	-	 	               DYLD-STUB$$swift_release
3.00 Mc    0.1%	-	 	              destroy for PredicateExpressions.Conjunction
3.00 Mc    0.1%	-	 	              destroy for PredicateExpressions.Disjunction
1.00 Mc    0.0%	-	 	              protocol witness for PredicateExpression.evaluate(_:) in conformance PredicateExpressions.Disjunction<A, B>
1.00 Mc    0.0%	-	 	              ___chkstk_darwin
43.87 Mc    1.4%	-	 	             initializeWithCopy for PredicateExpressions.Disjunction
23.00 Mc    0.7%	-	 	             destroy for PredicateExpressions.Disjunction
8.00 Mc    0.2%	8.00 Mc	 	             PredicateExpressions.Disjunction.evaluate(_:)
5.00 Mc    0.1%	-	 	             destroy for PredicateExpressions.Equal
3.00 Mc    0.1%	-	 	             destroy for PredicateExpressions.Conjunction
1.00 Mc    0.0%	-	 	             swift_release
1.00 Mc    0.0%	-	 	             destroy for PredicateExpressions.KeyPath
3.00 Mc    0.1%	-	 	            destroy for PredicateExpressions.Disjunction
2.00 Mc    0.0%	-	 	            ___chkstk_darwin
1.00 Mc    0.0%	-	 	            initializeWithCopy for PredicateExpressions.Disjunction
250.80 Mc    8.3%	-	 	           PredicateBindings.init<each A>(_:)
102.35 Mc    3.4%	-	 	           bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int)
63.10 Mc    2.1%	-	 	           __swift_instantiateGenericMetadata
...

This ties together with my question here:

I think that performance of predicates is a crucial feature as mentioned earlier up thread, without it many use cases are precluded - it seems key paths throws a spanner in the wheel here - are you aware of this performance problem and aware of any efforts to alleviate it by improving keypaths?

We will try to create proper issues on this if needed, but the simple benchmark has code like this:

func makeFoundationPredicate(_ frostflake: Frostflake, userId: UserIdentifier, accountId: AccountIdentifier) -> Predicate<Transaction> {
    let wrongId = frostflake.generate()
    // let now = InternalUTCClock.now
    // let secBefore = now.advanced(by: .seconds(-1))
    // let secAfter = now.advanced(by: .seconds(1))
    return #Predicate<Transaction> { transaction in
        ((transaction.user == userId) && ((transaction.account == accountId) || (transaction.account == wrongId)))
        || ((transaction.account == accountId)
            || ((transaction.id == wrongId) /*|| (transaction.created >= secBefore) && (transaction.created <= secAfter)*/))
    }
}
...
    Benchmark("FoundationPredicate #1 - evaluate", configuration: .init(scalingFactor: .kilo)) { benchmark in
        let users = (1...5).map { _ in UserIdentifier(frostflake.generate()) }
        let accounts = (1...5).map { _ in AccountIdentifier(frostflake.generate()) }
        let transactions = makeTransactions(count: 100, users, accounts)
        let predicate = makeFoundationPredicate(frostflake, userId: users[0], accountId: accounts[0])
        var matched = 0
        benchmark.startMeasurement()
        for idx in benchmark.scaledIterations {
            if try predicate.evaluate(transactions[idx % transactions.count]) {
                matched += 1
            }
        }
        benchmark.stopMeasurement()
        blackHole(matched)
    }

EDIT:
Another sincere question, are key paths truly required to implement predicates evaluation? (we primarily care about the hot evaluation path, not so much about serialisation / inspection / setup)

7 Likes