Memoization is an optimization technique that enhances the performance of deterministic methods by caching their results. Metalama provides a straightforward and high-performance implementation of this technique through the [Memoize] aspect.
Currently, this aspect is limited to get-only properties and parameterless methods. The cached value of memoized methods and properties is stored in a field of the object itself, enabling a high-performance implementation using Interlocked.CompareExchange
. It serves as an alternative to the Lazy<T> class, offering a simpler usage and superior performance characteristics.
To memoize a property or a method:
- Add the Metalama.Patterns.Memoization package into your project.
- Apply the [Memoize] attribute to the get-only property or parameterless method.
Warning
The current implementation of the [Memoize] aspect does not guarantee that the method will be executed only once. However, it does ensure that it always returns the same value or object.
Note
For nullable reference types and for value types, the cached value is stored in a StrongBox<T>, adding some memory allocation overhead in cases where many memoized properties or methods are evaluated. Nevertheless, this allows for minimal memory allocation when few or none of them are evaluated.
Example: Memoization
The following example demonstrates a typical use of the [Memoize] aspect. It presents a HashedBuffer
class, for which we aim to optimize the performance of the Hash
property and the ToString
method. We assume that these members are only evaluated for a minority of instances of the HashedBuffer
class, therefore the hash should not be pre-computed in the constructor. However, when they are evaluated, we assume they are evaluated often, which means that we should cache the result. The [Memoize] aspect offers a solution that is both simpler and more performant than the Lazy<T> class.
1using Metalama.Patterns.Memoization;
2using System;
3using System.IO.Hashing;
4
5namespace Doc.Memoize_;
6
7public class HashedBuffer
8{
9 public HashedBuffer( ReadOnlyMemory<byte> buffer )
10 {
11 this.Buffer = buffer;
12 }
13
14 public ReadOnlyMemory<byte> Buffer { get; }
15
16 [Memoize]
17 public ReadOnlyMemory<byte> Hash => XxHash64.Hash( this.Buffer.Span );
18
19 [Memoize]
20 public override string ToString() => $"{{HashedBuffer ({this.Buffer.Length} bytes)}}";
21}
1using Metalama.Patterns.Memoization;
2using System;
3using System.IO.Hashing;
4using System.Runtime.CompilerServices;
5
6namespace Doc.Memoize_;
7
8public class HashedBuffer
9{
10 public HashedBuffer(ReadOnlyMemory<byte> buffer)
11 {
12 this.Buffer = buffer;
13 }
14
15 public ReadOnlyMemory<byte> Buffer { get; }
16
17 [Memoize]
18 public ReadOnlyMemory<byte> Hash
19 {
20 get
21 {
22 if (_Hash == null)
23 {
24 var value = new StrongBox<ReadOnlyMemory<byte>>(Hash_Source);
25 global::System.Threading.Interlocked.CompareExchange(ref this._Hash, value, null);
26 }
27
28 return _Hash.Value;
29 }
30 }
31
32 private ReadOnlyMemory<byte> Hash_Source => XxHash64.Hash(this.Buffer.Span);
33
34 [Memoize]
35 public override string ToString()
36 {
37 if (_ToString == null)
38 {
39 string value;
40 value = $"{{HashedBuffer ({this.Buffer.Length} bytes)}}";
41 global::System.Threading.Interlocked.CompareExchange(ref this._ToString, value, null);
42 }
43
44 return _ToString;
45 }
46
47 private StrongBox<ReadOnlyMemory<byte>> _Hash;
48 private string _ToString;
49}
Memoization vs Caching
Memoization can be considered as a simple form of caching. The [Memoize] aspect is often a no-brainer, extremely simple to use, and requires no infrastructure.
Factor | Memoization | Caching |
---|---|---|
Scope | Local to a single class instance within the current process. | Either local or shared, when run as an external service such as Redis. |
Unicity of cache items | Specific to the current instance or type. | Based on explicit string cache keys. |
Complexity & overhead | Minimal overhead. | Significant overhead related to the generation of cache keys and, in case of distributed caching, serialization. |
Expiration & invalidation | No expiration or invalidation. | Advanced and configurable expiration policies and invalidation APIs. |