Skip to content

Commit

Permalink
Add updated benchmarks (#138)
Browse files Browse the repository at this point in the history
* Add updated benchmarks

Let's add updated benchmarks to get a better understanding of the
performance of case key paths vs. reflection-based case paths.

While case key paths bring a number of improvements to case paths,
including better ergonomics and the ability to utilize dynamic member
lookup along enum cases, they are currently a bit slower than
reflection-based case paths.

The performance of reflection-based case paths is the result of a long
journey of improvements, so this shouldn't be super surprising, and case
key paths are still plenty fast compared to earlier case paths.

These benchmarks will help us measure improvements to case key paths
over time.

* Performance: Access case path directly in dynamic member lookup (#137)

* Performance: Access case path directly in dynamic member lookup

* update bench
  • Loading branch information
stephencelis authored Jan 3, 2024
1 parent bba1111 commit d6db277
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 43 deletions.
6 changes: 4 additions & 2 deletions Sources/CasePaths/CasePathable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,10 @@ extension CasePathable {
/// userActions.compactMap(\.home) // [HomeAction.onAppear]
/// userActions.compactMap(\.settings) // [SettingsAction.subscribeButtonTapped]
/// ```
public subscript<Value>(dynamicMember keyPath: CaseKeyPath<Self, Value>) -> Value? {
self[case: keyPath]
public subscript<Value>(
dynamicMember keyPath: KeyPath<Self.AllCasePaths, AnyCasePath<Self, Value>>
) -> Value? {
Self.allCasePaths[keyPath: keyPath].extract(from: self)
}

/// Tests the associated value of a case.
Expand Down
7 changes: 7 additions & 0 deletions Sources/swift-case-paths-benchmark/Common.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@inline(__always)
func doNotOptimizeAway<T>(_ x: T) {
@_optimize(none)
func assumePointeeIsRead(_ x: UnsafeRawPointer) {}

withUnsafePointer(to: x) { assumePointeeIsRead($0) }
}
63 changes: 63 additions & 0 deletions Sources/swift-case-paths-benchmark/Foo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#if swift(>=5.9)
import CasePaths

@CasePathable
@dynamicMemberLookup
public enum Foo {
case foo(Foo2)
}

@CasePathable
@dynamicMemberLookup
public enum Foo2 {
case foo(Foo3)
}

@CasePathable
@dynamicMemberLookup
public enum Foo3 {
case foo(Foo4)
}

@CasePathable
@dynamicMemberLookup
public enum Foo4 {
case foo(Foo5)
}

@CasePathable
@dynamicMemberLookup
public enum Foo5 {
case foo(Foo6)
}

@CasePathable
@dynamicMemberLookup
public enum Foo6 {
case foo(Foo7)
}

@CasePathable
@dynamicMemberLookup
public enum Foo7 {
case foo(Foo8)
}

@CasePathable
@dynamicMemberLookup
public enum Foo8 {
case foo(Foo9)
}

@CasePathable
@dynamicMemberLookup
public enum Foo9 {
case foo(Foo10)
}

@CasePathable
@dynamicMemberLookup
public enum Foo10 {
case bar
}
#endif
182 changes: 141 additions & 41 deletions Sources/swift-case-paths-benchmark/main.swift
Original file line number Diff line number Diff line change
@@ -1,54 +1,154 @@
#if swift(<5.9)
import Benchmark
import CasePaths
import Benchmark
import CasePaths

enum Enum {
case associatedValue(Int)
case anotherAssociatedValue(String)
}
/*
name time std iterations
--------------------------------------------------------------------------------------------
Case Path Reflection (Appended: 2): Embed 250.000 ns ± 31.24 % 1000000
Case Path Reflection (Appended: 2): Extract 708.000 ns ± 32.00 % 1000000
Case Path Reflection (Appended: 2, Cached): Embed 41.000 ns ± 67.30 % 1000000
Case Path Reflection (Appended: 2, Cached): Extract 333.000 ns ± 43.49 % 1000000
Case Key Path (Appended: 2): Embed 4083.000 ns ± 9.37 % 346309
Case Key Path (Appended: 2): Extract 4541.000 ns ± 6.09 % 307882
Case Key Path (Appended: 2, Cached): Embed 1958.000 ns ± 17.72 % 709441
Case Key Path (Appended: 2, Cached): Extract 2417.000 ns ± 118.76 % 572773
Case Key Path (Appended: 2, Cached + Converted): Embed 958.000 ns ± 14.47 % 1000000
Case Key Path (Appended: 2, Cached + Converted): Extract 1417.000 ns ± 15.08 % 990935
Case Path Reflection (Appended: 10): Embed 1709.000 ns ± 8.49 % 816699
Case Path Reflection (Appended: 10): Extract 4750.000 ns ± 6.49 % 293788
Case Path Reflection (Appended: 10, Cached): Embed 83.000 ns ± 28.79 % 1000000
Case Path Reflection (Appended: 10, Cached): Extract 1917.000 ns ± 13.23 % 723330
Case Key Path (Appended: 10): Embed 12416.000 ns ± 15.25 % 112901
Case Key Path (Appended: 10): Extract 13833.000 ns ± 167.15 % 102161
Case Key Path (Appended: 10, Cached): Embed 8000.000 ns ± 4.86 % 175509
Case Key Path (Appended: 10, Cached): Extract 9333.000 ns ± 5.80 % 150034
Case Key Path (Appended: 10, Cached + Converted): Embed 3625.000 ns ± 5.56 % 381696
Case Key Path (Appended: 10, Cached + Converted): Extract 4875.000 ns ± 8.08 % 285089
Case Pathable (Dynamic Member Lookup: 10) 0.000 ns ± inf % 1000000
*/

benchmark("Case Path Reflection (Appended: 2): Embed") {
let cp = /Result<Int?, any Error>.success .. /Int?.some
doNotOptimizeAway(cp.embed(42))
}
benchmark("Case Path Reflection (Appended: 2): Extract") {
let cp = /Result<Int?, any Error>.success .. /Int?.some
doNotOptimizeAway(cp.extract(from: .success(.some(42))))
}

let enumCase = Enum.associatedValue(42)
let anotherCase = Enum.anotherAssociatedValue("Blob")
let cp = /Result<Int?, any Error>.success .. /Int?.some
benchmark("Case Path Reflection (Appended: 2, Cached): Embed") {
doNotOptimizeAway(cp.embed(42))
}
benchmark("Case Path Reflection (Appended: 2, Cached): Extract") {
doNotOptimizeAway(cp.extract(from: .success(.some(42))))
}

let manual = AnyCasePath(
embed: Enum.associatedValue,
extract: {
guard case let .associatedValue(value) = $0 else { return nil }
return value
}
)
let reflection: AnyCasePath<Enum, Int> = /Enum.associatedValue
benchmark("Case Key Path (Appended: 2): Embed") {
let ckp: CaseKeyPath<Result<Int?, any Error>, Int> = \.success.some
doNotOptimizeAway(ckp(42))
}
benchmark("Case Key Path (Appended: 2): Extract") {
let ckp: CaseKeyPath<Result<Int?, any Error>, Int> = \.success.some
doNotOptimizeAway(Result<Int?, any Error>.success(.some(42))[case: ckp])
}

let success = BenchmarkSuite(name: "Success") {
$0.benchmark("Manual") {
precondition(manual.extract(from: enumCase) == 42)
}
let ckp: CaseKeyPath<Result<Int?, any Error>, Int> = \.success.some
benchmark("Case Key Path (Appended: 2, Cached): Embed") {
doNotOptimizeAway(ckp(42))
}
benchmark("Case Key Path (Appended: 2, Cached): Extract") {
doNotOptimizeAway(Result<Int?, any Error>.success(.some(42))[case: ckp])
}

$0.benchmark("Reflection") {
precondition(reflection.extract(from: enumCase) == 42)
}
let acp = AnyCasePath(ckp)
benchmark("Case Key Path (Appended: 2, Cached + Converted): Embed") {
doNotOptimizeAway(acp.embed(42))
}
benchmark("Case Key Path (Appended: 2, Cached + Converted): Extract") {
doNotOptimizeAway(acp.extract(from: .success(.some(42))))
}

$0.benchmark("Reflection (uncached)") {
precondition((/Enum.associatedValue).extract(from: enumCase) == 42)
}
#if swift(>=5.9)
benchmark("Case Path Reflection (Appended: 10): Embed") {
let cp = (/Foo.foo)
.appending(path: /Foo2.foo)
.appending(path: /Foo3.foo)
.appending(path: /Foo4.foo)
.appending(path: /Foo5.foo)
.appending(path: /Foo6.foo)
.appending(path: /Foo7.foo)
.appending(path: /Foo8.foo)
.appending(path: /Foo9.foo)
.appending(path: /Foo10.bar)
doNotOptimizeAway(cp.embed(()))
}
benchmark("Case Path Reflection (Appended: 10): Extract") {
let cp = (/Foo.foo)
.appending(path: /Foo2.foo)
.appending(path: /Foo3.foo)
.appending(path: /Foo4.foo)
.appending(path: /Foo5.foo)
.appending(path: /Foo6.foo)
.appending(path: /Foo7.foo)
.appending(path: /Foo8.foo)
.appending(path: /Foo9.foo)
.appending(path: /Foo10.bar)
doNotOptimizeAway(cp.extract(from: .foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.bar)))))))))))
}

let failure = BenchmarkSuite(name: "Failure") {
$0.benchmark("Manual") {
precondition(manual.extract(from: anotherCase) == nil)
}
let cp2 = (/Foo.foo)
.appending(path: /Foo2.foo)
.appending(path: /Foo3.foo)
.appending(path: /Foo4.foo)
.appending(path: /Foo5.foo)
.appending(path: /Foo6.foo)
.appending(path: /Foo7.foo)
.appending(path: /Foo8.foo)
.appending(path: /Foo9.foo)
.appending(path: /Foo10.bar)
benchmark("Case Path Reflection (Appended: 10, Cached): Embed") {
doNotOptimizeAway(cp2.embed(()))
}
benchmark("Case Path Reflection (Appended: 10, Cached): Extract") {
doNotOptimizeAway(cp2.extract(from: .foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.bar)))))))))))
}

$0.benchmark("Reflection") {
precondition(reflection.extract(from: anotherCase) == nil)
}
benchmark("Case Key Path (Appended: 10): Embed") {
let ckp: CaseKeyPath<Foo, Void> = \.foo.foo.foo.foo.foo.foo.foo.foo.foo.bar
doNotOptimizeAway(ckp(()))
}
benchmark("Case Key Path (Appended: 10): Extract") {
let ckp: CaseKeyPath<Foo, Void> = \.foo.foo.foo.foo.foo.foo.foo.foo.foo.bar
doNotOptimizeAway(
Foo.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.bar)))))))))[case: ckp]
)
}

$0.benchmark("Reflection (uncached)") {
precondition((/Enum.associatedValue).extract(from: anotherCase) == nil)
}
let ckp2: CaseKeyPath<Foo, Void> = \.foo.foo.foo.foo.foo.foo.foo.foo.foo.bar
benchmark("Case Key Path (Appended: 10, Cached): Embed") {
doNotOptimizeAway(ckp2(()))
}
benchmark("Case Key Path (Appended: 10, Cached): Extract") {
doNotOptimizeAway(Foo.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.bar)))))))))[case: ckp2])
}

let acp2 = AnyCasePath(ckp2)
benchmark("Case Key Path (Appended: 10, Cached + Converted): Embed") {
doNotOptimizeAway(acp2.embed(()))
}
benchmark("Case Key Path (Appended: 10, Cached + Converted): Extract") {
doNotOptimizeAway(
acp2.extract(from: Foo.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.bar))))))))))
)
}

Benchmark.main([
success,
failure,
])
benchmark("Case Pathable (Dynamic Member Lookup: 10)") {
let foo = Foo.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.foo(.bar)))))))))
doNotOptimizeAway(foo.foo?.foo?.foo?.foo?.foo?.foo?.foo?.foo?.foo?.bar)
}
#endif

Benchmark.main([
defaultBenchmarkSuite,
])

0 comments on commit d6db277

Please sign in to comment.