-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
bba1111
commit d6db277
Showing
4 changed files
with
215 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]) |