Skip to content
This repository was archived by the owner on Nov 27, 2025. It is now read-only.

Commit 93a9ab6

Browse files
committed
Big (2x - 5x) perf gain via momoisation
1 parent 4e7ea73 commit 93a9ab6

2 files changed

Lines changed: 41 additions & 26 deletions

File tree

src/Hedgehog.Experimental.Tests/Hedgehog.Experimental.Tests.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<OutputType>Exe</OutputType>
66
<GenerateProgramFile>false</GenerateProgramFile>
77
</PropertyGroup>
8-
8+
99
<ItemGroup>
1010
<Compile Include="ComplexGenericTest.fs" />
1111
<Compile Include="TypeParamMappingTests.fs" />

src/Hedgehog.Experimental/GeneratorCollection.fs

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Hedgehog
22

33
open System
4+
open System.Collections.Concurrent
45
open System.Collections.Immutable
56

67
// A generator factory which can be backed by a generic method.
@@ -21,8 +22,7 @@ type internal GeneratorKey = {
2122
ConcreteTypes: Type option list
2223
}
2324

24-
[<Struct>]
25-
type internal GeneratorCollection =
25+
type internal GeneratorCollection = {
2626
// A dictionary of generators.
2727
// The key distinguishes between different patterns of generic vs concrete type arguments
2828
// The value is a tuple of:
@@ -31,14 +31,17 @@ type internal GeneratorCollection =
3131
// 3. A generator factory, which can be backed by a generic method,
3232
// so it takes an array of genetic type parameters,
3333
// and an array of arguments to create the generator.
34-
GeneratorCollection of ImmutableDictionary<GeneratorKey, Type * Type[] * GeneratorFactory>
34+
Generators: ImmutableDictionary<GeneratorKey, Type * Type[] * GeneratorFactory>
35+
// The cache memoizes tryFindFor results to avoid repeated reflection calls.
36+
Cache: ConcurrentDictionary<Type, (Type * (Type[] * GeneratorFactory)) option>
37+
}
3538

3639
module internal GeneratorCollection =
3740

38-
let empty = GeneratorCollection(ImmutableDictionary.Empty)
39-
40-
let unwrap (GeneratorCollection map) = map
41-
let map f = unwrap >> f >> GeneratorCollection
41+
let empty = {
42+
Generators = ImmutableDictionary.Empty
43+
Cache = ConcurrentDictionary<Type, (Type * (Type[] * GeneratorFactory)) option>()
44+
}
4245

4346
/// Create a GeneratorKey from a type by identifying which positions are generic vs concrete
4447
let private createKey (t: Type) : GeneratorKey =
@@ -52,12 +55,21 @@ module internal GeneratorCollection =
5255
// Non-generic types use themselves as the key
5356
{ GenericTypeDefinition = t; ConcreteTypes = [] }
5457

55-
let merge (GeneratorCollection gens1) (GeneratorCollection gens2) =
56-
GeneratorCollection (gens1.SetItems(gens2))
58+
let merge (gens1: GeneratorCollection) (gens2: GeneratorCollection) =
59+
// Create a new cache when merging since the generator set has changed
60+
{
61+
Generators = gens1.Generators.SetItems(gens2.Generators)
62+
Cache = ConcurrentDictionary<Type, (Type * (Type[] * GeneratorFactory)) option>()
63+
}
5764

5865
let addGenerator (normalizedType: Type) (originalType: Type) (paramTypes: Type[]) (factory: Type[] -> obj[] -> obj) =
59-
let key = createKey normalizedType
60-
map _.SetItem(key, (originalType, paramTypes, factory))
66+
fun (gc: GeneratorCollection) ->
67+
let key = createKey normalizedType
68+
// Reset cache when adding a generator since lookup results may change
69+
{
70+
Generators = gc.Generators.SetItem(key, (originalType, paramTypes, factory))
71+
Cache = ConcurrentDictionary<Type, (Type * (Type[] * GeneratorFactory)) option>()
72+
}
6173

6274
/// Count the number of generic parameters in a type
6375
let private countGenericParameters (t: Type) =
@@ -69,20 +81,23 @@ module internal GeneratorCollection =
6981
// to satisfy specific types (like Either<int, string>).
7082
// When multiple generators match, returns the most specific one (fewest generic parameters).
7183
// Returns the original reflected type along with the args and factory.
72-
let tryFindFor (targetType: Type) =
73-
unwrap
74-
>> Seq.choose (fun (KeyValue (key, (originalType, paramTypes, factory))) ->
75-
// Only consider generators with the same generic type definition
76-
let targetKey = createKey targetType
77-
if key.GenericTypeDefinition = targetKey.GenericTypeDefinition then
78-
// Check if the stored type can satisfy the target type
79-
if originalType |> TypeUtils.satisfies targetType then
80-
Some (originalType, paramTypes, factory)
84+
// Results are memoized to avoid repeated reflection calls.
85+
let tryFindFor (targetType: Type) (gc: GeneratorCollection) =
86+
gc.Cache.GetOrAdd(targetType, fun targetType ->
87+
let targetKey = createKey targetType
88+
gc.Generators
89+
|> Seq.choose (fun (KeyValue (key, (originalType, paramTypes, factory))) ->
90+
// Only consider generators with the same generic type definition
91+
if key.GenericTypeDefinition = targetKey.GenericTypeDefinition then
92+
// Check if the stored type can satisfy the target type
93+
if originalType |> TypeUtils.satisfies targetType then
94+
Some (originalType, paramTypes, factory)
95+
else
96+
None
8197
else
8298
None
83-
else
84-
None
99+
)
100+
|> Seq.sortBy (fun (originalType, _, _) -> countGenericParameters originalType)
101+
|> Seq.tryHead
102+
|> Option.map (fun (originalType, paramTypes, factory) -> (originalType, (paramTypes, factory)))
85103
)
86-
>> Seq.sortBy (fun (originalType, _, _) -> countGenericParameters originalType)
87-
>> Seq.tryHead
88-
>> Option.map (fun (originalType, paramTypes, factory) -> (originalType, (paramTypes, factory)))

0 commit comments

Comments
 (0)