|
1 | 1 | using System.Buffers; |
2 | 2 | using System.Diagnostics.CodeAnalysis; |
| 3 | +using System.Runtime.CompilerServices; |
3 | 4 |
|
4 | 5 | namespace RESPite.Buffers; |
5 | 6 |
|
6 | | -[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] |
7 | | -public abstract class CycleBufferPool |
| 7 | +internal static class MemoryPoolExtensions |
8 | 8 | { |
9 | | - /// <summary> |
10 | | - /// Create an initial buffer. |
11 | | - /// </summary> |
12 | | - public virtual IMemoryOwner<byte> Rent() => Rent(default); |
| 9 | + internal static IMemoryOwner<T> Rent<T>(this MemoryPool<T> pool, in ReadOnlySequence<T> existing) |
| 10 | + { |
| 11 | + return pool is CycleBufferPool<T> typed |
| 12 | + ? typed.Rent(existing) |
| 13 | + : pool.Rent(Math.Min(pool.MaxBufferSize, NextSize(existing))); |
| 14 | + } |
| 15 | + |
| 16 | + private const int DefaultPageSizeBytes = 8 * 1024; |
| 17 | + |
| 18 | + internal static int NextSize<T>(in ReadOnlySequence<T> existing) |
| 19 | + { |
| 20 | + if (existing.IsEmpty) return DefaultPageSizeBytes / Unsafe.SizeOf<T>(); |
| 21 | + |
| 22 | + // use a growth strategy looking at the size of the last segment |
| 23 | + int lastChunk; |
| 24 | + if (existing.IsSingleSegment) |
| 25 | + { |
| 26 | + lastChunk = existing.First.Length; |
| 27 | + } |
| 28 | + else if (existing.End.GetObject() is ReadOnlySequenceSegment<byte> segment) |
| 29 | + { |
| 30 | + lastChunk = segment.Memory.Length; // note we ignore GetInteger() intentionally |
| 31 | + } |
| 32 | + else |
| 33 | + { |
| 34 | + // do it the hard way; note we'll only observe the reserved size, rather |
| 35 | + // than the actual buffer size, but that's the best we can do |
| 36 | + lastChunk = 0; |
| 37 | + foreach (var chunk in existing) |
| 38 | + { |
| 39 | + if (!chunk.IsEmpty) lastChunk = chunk.Length; |
| 40 | + } |
| 41 | + |
| 42 | + if (lastChunk is 0) lastChunk = existing.First.Length; // paranoia |
| 43 | + } |
| 44 | + |
| 45 | + // apply a fixed lower bound - don't start with trivial growth |
| 46 | + lastChunk = Math.Max(lastChunk, 16); |
| 47 | + |
| 48 | + // apply doubling; "max" here is to account for overflow - i.e. stop growing if that happens (unlikely) |
| 49 | + return Math.Max(lastChunk, lastChunk << 1); |
| 50 | + } |
| 51 | +} |
13 | 52 |
|
| 53 | +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] |
| 54 | +public abstract class CycleBufferPool<T> : MemoryPool<T> |
| 55 | +{ |
14 | 56 | /// <summary> |
15 | 57 | /// Create a buffer with knowledge of the existing leased data. |
16 | 58 | /// </summary> |
17 | | - public abstract IMemoryOwner<byte> Rent(in ReadOnlySequence<byte> existing); |
| 59 | + public abstract IMemoryOwner<T> Rent(in ReadOnlySequence<T> existing); |
18 | 60 |
|
19 | 61 | // new MemoryPool(...) would be a non-growing buffer pool. |
20 | | - public static CycleBufferPool Default { get; } = new GrowingMemoryPool(minBytes: 8 * 1024); |
| 62 | + public static CycleBufferPool<T> Default { get; } = new DefaultPool(); |
21 | 63 |
|
22 | | - private class MemoryPool : CycleBufferPool |
| 64 | + private class DefaultPool : CycleBufferPool<T> |
23 | 65 | { |
| 66 | + private static readonly MemoryPool<T> Tail = |
24 | 67 | #if TRACK_MEMORY |
25 | | - private static MemoryPool<byte> DefaultPool => MemoryTrackedPool<byte>.Shared; |
| 68 | + MemoryTrackedPool<T>.Shared; |
26 | 69 | #else |
27 | | - private static MemoryPool<byte> DefaultPool => MemoryPool<byte>.Shared; |
| 70 | + MemoryPool<T>.Shared; |
28 | 71 | #endif |
29 | | - private readonly MemoryPool<byte> _pool; |
30 | | - private readonly int _minBytes, _maxBytes; |
31 | 72 |
|
32 | | - public MemoryPool(int minBytes, MemoryPool<byte>? pool = null, int maxBytes = int.MaxValue) |
33 | | - { |
34 | | - _pool = pool ?? DefaultPool; |
35 | | - // capture the max bytes, without exceeding the pool's max size |
36 | | - _maxBytes = Math.Min(maxBytes, _pool.MaxBufferSize); |
37 | | - // capture the min bytes, applying a rigid lower bound, and not overlapping the max bytes |
38 | | - _minBytes = Math.Min(Math.Max(minBytes, 16), _maxBytes); |
39 | | - } |
| 73 | + // ReSharper disable once StaticMemberInGenericType - intentional to memoize, but can vary per T |
| 74 | + private static readonly int MaxSize = Tail.MaxBufferSize; |
40 | 75 |
|
41 | | - /// <summary> |
42 | | - /// Rent a chunk using the specified size as a hint. |
43 | | - /// </summary> |
44 | | - protected IMemoryOwner<byte> Rent(int bytes) |
45 | | - { |
46 | | -#if NET |
47 | | - bytes = Math.Clamp(bytes, _minBytes, _maxBytes); |
48 | | -#else |
49 | | - bytes = Math.Min(Math.Max(bytes, _minBytes), _maxBytes); |
50 | | -#endif |
51 | | - return _pool.Rent(bytes); |
52 | | - } |
53 | | - |
54 | | - // by default, use fixed size without reference to the existing data; subclasses can tweak |
55 | | - |
56 | | - /// <inheritdoc/> |
57 | | - public override IMemoryOwner<byte> Rent() => Rent(_minBytes); |
| 76 | + protected override void Dispose(bool disposing) { } |
58 | 77 |
|
59 | 78 | /// <inheritdoc/> |
60 | | - public override IMemoryOwner<byte> Rent(in ReadOnlySequence<byte> existing) => Rent(_minBytes); |
61 | | - } |
| 79 | + public override IMemoryOwner<T> Rent(int minBufferSize = -1) => Tail.Rent(minBufferSize); |
62 | 80 |
|
63 | | - private sealed class GrowingMemoryPool(int minBytes, MemoryPool<byte>? pool = null, int maxBytes = int.MaxValue) |
64 | | - : MemoryPool(minBytes, pool, maxBytes) |
65 | | - { |
66 | | - public override IMemoryOwner<byte> Rent(in ReadOnlySequence<byte> existing) |
67 | | - { |
68 | | - if (existing.IsEmpty) return base.Rent(existing); |
69 | | - // use a growth strategy looking at the size of the last segment |
70 | | - int lastChunk; |
71 | | - if (existing.IsSingleSegment) |
72 | | - { |
73 | | - lastChunk = existing.First.Length; |
74 | | - } |
75 | | - else if (existing.End.GetObject() is ReadOnlySequenceSegment<byte> segment) |
76 | | - { |
77 | | - lastChunk = segment.Memory.Length; // note we ignore GetInteger() intentionally |
78 | | - } |
79 | | - else |
80 | | - { |
81 | | - // do it the hard way; note we'll only observe the reserved size, rather |
82 | | - // than the actual buffer size, but that's the best we can do |
83 | | - lastChunk = 0; |
84 | | - foreach (var chunk in existing) |
85 | | - { |
86 | | - if (!chunk.IsEmpty) lastChunk = chunk.Length; |
87 | | - } |
88 | | - |
89 | | - if (lastChunk is 0) lastChunk = existing.First.Length; // paranoia |
90 | | - } |
| 81 | + public override int MaxBufferSize => Tail.MaxBufferSize; |
91 | 82 |
|
92 | | - // "max" here is to account for overflow - i.e. stop growing if that happens (unlikely) |
93 | | - return Rent(Math.Max(lastChunk, lastChunk << 1)); |
94 | | - } |
| 83 | + /// <inheritdoc/> |
| 84 | + public override IMemoryOwner<T> Rent(in ReadOnlySequence<T> existing) |
| 85 | + => Tail.Rent(Math.Min(MemoryPoolExtensions.NextSize(existing), MaxSize)); |
95 | 86 | } |
96 | 87 | } |
0 commit comments