Open sandboxFocusImprove this doc

Metalama.Patterns.Memoization

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:

  1. Add the Metalama.Patterns.Memoization package into your project.
  2. 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.

Source Code
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}
Transformed Code
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.