Skip to content

Commit 73eb777

Browse files
author
LoneWandererProductions
committed
improve blob
1 parent 0420b8a commit 73eb777

1 file changed

Lines changed: 80 additions & 75 deletions

File tree

MemoryManager.Core/BlobManager.cs

Lines changed: 80 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ namespace MemoryManager.Core
1111
/// <inheritdoc />
1212
/// <summary>
1313
/// A Linear/Bump allocator designed for large or unpredictable blobs of memory.
14-
/// Allocations are extremely fast, but memory is only reclaimed when the entire
15-
/// manager is compacted/cleared.
14+
/// Allocations are extremely fast, leveraging a dense flat array for metadata tracking.
15+
/// Memory is physically reclaimed and consolidated during explicit compaction.
1616
/// </summary>
1717
/// <seealso cref="IMemoryLane" />
1818
public sealed class BlobManager : IMemoryLane
@@ -43,9 +43,9 @@ public sealed class BlobManager : IMemoryLane
4343
private int _nextFreeOffset;
4444

4545
/// <summary>
46-
/// Maps the handle IDs to their actual blob metadata.
46+
/// Dense flat array mapping handles directly to their metadata via (StartingId - Id)
4747
/// </summary>
48-
private readonly Dictionary<int, BlobEntry> _entries = new();
48+
private BlobEntry[] _entries = new BlobEntry[128];
4949

5050
#if DEBUG
5151
/// <summary>
@@ -55,16 +55,12 @@ public sealed class BlobManager : IMemoryLane
5555
#endif
5656

5757
/// <inheritdoc />
58-
public int EntryCount => _entries.Count;
58+
public int EntryCount { get; private set; }
5959

6060
/// <inheritdoc />
6161
public event Action<string>? OnCompaction;
6262

6363
/// <inheritdoc />
64-
/// <summary>#
65-
/// Defined by interface, but not utilized in a fixed-size bump allocator
66-
/// Occurs when [on allocation extension].
67-
/// </summary>
6864
public event Action<string, int, int>? OnAllocationExtension;
6965

7066
/// <summary>
@@ -77,18 +73,12 @@ public BlobManager(nint buffer, int capacity)
7773
}
7874

7975
/// <inheritdoc />
80-
/// <summary>
81-
/// Checks if the memory lane can allocate a block of the specified size.
82-
/// </summary>
8376
public bool CanAllocate(int size)
8477
{
8578
return _nextFreeOffset + size <= _capacity;
8679
}
8780

8881
/// <inheritdoc />
89-
/// <summary>
90-
/// Allocates a contiguous block of memory by bumping the offset forward.
91-
/// </summary>
9282
public MemoryHandle Allocate(
9383
int size,
9484
AllocationPriority priority = AllocationPriority.Normal,
@@ -100,19 +90,20 @@ public MemoryHandle Allocate(
10090
throw new OutOfMemoryException(
10191
$"BlobManager: Out of memory. Requested {size} bytes, but only {FreeSpace()} available.");
10292

103-
// Prevent negative ID underflow wrapping into positive integers
10493
if (_nextId == int.MinValue)
105-
throw new OutOfMemoryException("BlobManager: ID exhaustion. Cannot allocate more IDs without violating negative ID convention.");
94+
throw new OutOfMemoryException("BlobManager: ID exhaustion. Cannot allocate more IDs.");
10695

10796
var id = _nextId--;
108-
var allocatedOffset = _nextFreeOffset;
97+
int index = StartingId - id; // Maps sequential negative IDs cleanly to 0, 1, 2...
98+
99+
EnsureCapacity(index);
109100

110101
byte version = 1;
111102

112-
_entries[id] = new BlobEntry
103+
_entries[index] = new BlobEntry
113104
{
114105
Id = id,
115-
Offset = allocatedOffset,
106+
Offset = _nextFreeOffset,
116107
Size = size,
117108
AllocationFrame = currentFrame,
118109
Version = version
@@ -127,29 +118,30 @@ public MemoryHandle Allocate(
127118

128119
// Bump the allocator forward
129120
_nextFreeOffset += size;
121+
EntryCount++;
130122

131123
return new MemoryHandle(id, version, this);
132124
}
133125

134126
/// <inheritdoc />
135-
/// <summary>
136-
/// Invalidates the handle.
137-
/// Note: As a bump allocator, this does NOT immediately reclaim the physical memory bytes.
138-
/// </summary>
139127
public void Free(MemoryHandle handle)
140128
{
141-
if (!_entries.Remove(handle.Id))
129+
int index = StartingId - handle.Id;
130+
131+
if (index < 0 || index >= _entries.Length || _entries[index].Size == 0)
142132
throw new InvalidOperationException($"BlobManager: Invalid or double-freed handle {handle.Id}");
143133

134+
_entries[index] = default;
135+
EntryCount--;
136+
144137
#if DEBUG
145138
_debugNames.Remove(handle.Id);
146139
#endif
147140
}
148141

149142
/// <summary>
150-
/// Frees the many.
143+
/// Frees multiple handles sequentially.
151144
/// </summary>
152-
/// <param name="handles">The handles.</param>
153145
public void FreeMany(IEnumerable<MemoryHandle> handles)
154146
{
155147
foreach (var handle in handles)
@@ -161,24 +153,36 @@ public void FreeMany(IEnumerable<MemoryHandle> handles)
161153
/// <inheritdoc />
162154
public nint Resolve(MemoryHandle handle)
163155
{
164-
if (!_entries.TryGetValue(handle.Id, out var entry))
156+
int index = StartingId - handle.Id;
157+
158+
if (index < 0 || index >= _entries.Length || _entries[index].Size == 0)
165159
throw new InvalidOperationException($"BlobManager: Invalid handle {handle.Id}");
166160

161+
ref readonly var entry = ref _entries[index];
162+
167163
if (entry.Version != handle.Version)
168164
throw new AccessViolationException($"Blob Zombie: ID {handle.Id} version mismatch.");
169165

170166
return _buffer + entry.Offset;
171167
}
172168

173169
/// <inheritdoc />
174-
public bool HasHandle(MemoryHandle handle) => _entries.ContainsKey(handle.Id);
170+
public bool HasHandle(MemoryHandle handle)
171+
{
172+
int index = StartingId - handle.Id;
173+
return index >= 0 && index < _entries.Length && _entries[index].Size > 0;
174+
}
175175

176176
/// <inheritdoc />
177177
public AllocationEntry GetEntry(MemoryHandle handle)
178178
{
179-
if (!_entries.TryGetValue(handle.Id, out var blob))
179+
int index = StartingId - handle.Id;
180+
181+
if (index < 0 || index >= _entries.Length || _entries[index].Size == 0)
180182
throw new InvalidOperationException($"BlobManager: Invalid handle {handle.Id}");
181183

184+
ref readonly var blob = ref _entries[index];
185+
182186
return new AllocationEntry
183187
{
184188
HandleId = blob.Id,
@@ -197,41 +201,38 @@ public AllocationEntry GetEntry(MemoryHandle handle)
197201
/// <inheritdoc />
198202
public int GetAllocationSize(MemoryHandle handle)
199203
{
200-
if (!_entries.TryGetValue(handle.Id, out var blob))
204+
int index = StartingId - handle.Id;
205+
206+
if (index < 0 || index >= _entries.Length || _entries[index].Size == 0)
201207
throw new InvalidOperationException($"BlobManager: Invalid handle {handle.Id}");
202208

203-
return blob.Size;
209+
return _entries[index].Size;
204210
}
205211

206212
/// <inheritdoc />
207-
/// <summary>
208-
/// WARNING: For a linear allocator, compaction means a total reset.
209-
/// This wipes all entries and resets the offset to 0.
210-
/// </summary>
211213
public void Compact()
212214
{
213-
if (_entries.Count == 0) return;
215+
if (EntryCount == 0) return;
214216

215-
// 1. Rent a temporary buffer for zero-allocation sorting
216217
var pool = System.Buffers.ArrayPool<BlobEntry>.Shared;
217-
var scratch = pool.Rent(_entries.Count);
218+
var scratch = pool.Rent(EntryCount);
218219

219220
try
220221
{
221222
var validCount = 0;
222-
foreach (var entry in _entries.Values)
223+
for (var i = 0; i < _entries.Length; i++)
223224
{
224-
scratch[validCount++] = entry;
225+
if (_entries[i].Size > 0)
226+
{
227+
scratch[validCount++] = _entries[i];
228+
}
225229
}
226230

227231
var validSpan = scratch.AsSpan(0, validCount);
228-
229-
// Sort natively using a struct to avoid delegate allocation
230232
validSpan.Sort(new BlobOffsetComparer());
231233

232234
var currentOffset = 0;
233235

234-
// 2. Slide each entry as far left as possible
235236
for (var i = 0; i < validSpan.Length; i++)
236237
{
237238
ref readonly var entry = ref validSpan[i];
@@ -240,7 +241,6 @@ public void Compact()
240241
{
241242
unsafe
242243
{
243-
// System.Buffer.MemoryCopy safely handles overlapping memory
244244
System.Buffer.MemoryCopy(
245245
(void*)(_buffer + entry.Offset),
246246
(void*)(_buffer + currentOffset),
@@ -249,15 +249,13 @@ public void Compact()
249249
}
250250
}
251251

252-
// 3. Update the entry's metadata with its new physical address
253-
var updatedEntry = entry;
254-
updatedEntry.Offset = currentOffset;
255-
_entries[entry.Id] = updatedEntry;
252+
// Update the offset inside our primary dense track tracking array slot directly
253+
int index = StartingId - entry.Id;
254+
_entries[index].Offset = currentOffset;
256255

257256
currentOffset += entry.Size;
258257
}
259258

260-
// 4. Update the global allocator offset
261259
_nextFreeOffset = currentOffset;
262260
}
263261
finally
@@ -271,30 +269,29 @@ public void Compact()
271269
/// <inheritdoc />
272270
public double UsagePercentage()
273271
{
274-
return _nextFreeOffset / (double)_capacity * 100.0;
272+
// Normalizes return behavior with FastLane/SlowLane metrics (returns scale 0.0 - 1.0 instead of 0.0 - 100.0)
273+
return _capacity == 0 ? 0.0 : _nextFreeOffset / (double)_capacity;
275274
}
276275

277276
/// <inheritdoc />
278277
public int FreeSpace() => _capacity - _nextFreeOffset;
279278

280279
/// <inheritdoc />
281-
/// <summary>
282-
/// Calculates the percentage of "dead" space behind the bump pointer
283-
/// that can be reclaimed via Compaction.
284-
/// </summary>
285280
public int EstimateFragmentation()
286281
{
287282
var allocatedBytes = _nextFreeOffset;
288283
if (allocatedBytes == 0) return 0;
289284

290285
var livingBytes = 0;
291-
foreach (var blob in _entries.Values)
286+
for (var i = 0; i < _entries.Length; i++)
292287
{
293-
livingBytes += blob.Size;
288+
if (_entries[i].Size > 0)
289+
{
290+
livingBytes += _entries[i].Size;
291+
}
294292
}
295293

296294
var wastedBytes = allocatedBytes - livingBytes;
297-
298295
return (int)((double)wastedBytes / allocatedBytes * 100);
299296
}
300297

@@ -304,29 +301,27 @@ public int EstimateFragmentation()
304301
/// <inheritdoc />
305302
public IEnumerable<MemoryHandle> GetHandles()
306303
{
307-
foreach (var entry in _entries.Values)
304+
for (var i = 0; i < _entries.Length; i++)
308305
{
309-
// Return the correct, versioned handle
310-
yield return new MemoryHandle(entry.Id, entry.Version, this);
306+
if (_entries[i].Size > 0)
307+
{
308+
yield return new MemoryHandle(_entries[i].Id, _entries[i].Version, this);
309+
}
311310
}
312311
}
313312

314313
/// <inheritdoc />
315314
public string DebugDump()
316315
{
317-
return $"BlobManager Dump\nAllocations: {_entries.Count}\nUsed: {_nextFreeOffset}/{_capacity} bytes";
316+
return $"BlobManager Dump\nAllocations: {EntryCount}\nUsed: {_nextFreeOffset}/{_capacity} bytes";
318317
}
319318

320319
/// <inheritdoc />
321-
/// <summary>
322-
/// Generates a visual string representation of the BlobManager's memory layout.
323-
/// █ = Living Data, - = Dead Gap (Wasted), ░ = Untouched Capacity
324-
/// </summary>
325320
public string DebugVisualMap()
326321
{
327322
if (_capacity == 0) return "[]";
328323

329-
const int mapResolution = 80; // Width of the console map
324+
const int mapResolution = 80;
330325
var map = new char[mapResolution];
331326
var bytesPerChar = (double)_capacity / mapResolution;
332327

@@ -335,19 +330,16 @@ public string DebugVisualMap()
335330
var startByte = i * bytesPerChar;
336331
var endByte = (i + 1) * bytesPerChar;
337332

338-
// If this bucket is completely past the bump pointer, it's untouched.
339333
if (startByte >= _nextFreeOffset)
340334
{
341335
map[i] = '░';
342336
continue;
343337
}
344338

345-
// Otherwise, check if any living blob intersects this bucket
346339
var isLiving = false;
347-
foreach (var blob in _entries.Values)
340+
for (var j = 0; j < _entries.Length; j++)
348341
{
349-
// Intersection math: Blob starts before bucket ends AND Blob ends after bucket starts
350-
if (blob.Offset < endByte && blob.Offset + blob.Size > startByte)
342+
if (_entries[j].Size > 0 && _entries[j].Offset < endByte && _entries[j].Offset + _entries[j].Size > startByte)
351343
{
352344
isLiving = true;
353345
break;
@@ -363,14 +355,27 @@ public string DebugVisualMap()
363355
/// <inheritdoc />
364356
public string DebugRedirections() => "[BlobRedirects not applicable]";
365357

366-
/// <inheritdoc />
358+
/// <summary>
359+
/// Resizes the tracking entry metadata array geometric pool size.
360+
/// </summary>
361+
private void EnsureCapacity(int requiredIndex)
362+
{
363+
if (requiredIndex >= _entries.Length)
364+
{
365+
var oldSize = _entries.Length;
366+
var newSize = oldSize * 2;
367+
if (newSize <= requiredIndex) newSize = requiredIndex + 1;
368+
369+
Array.Resize(ref _entries, newSize);
370+
OnAllocationExtension?.Invoke(nameof(BlobManager), oldSize, newSize);
371+
}
372+
}
373+
367374
/// <summary>
368375
/// Zero-allocation comparer for sorting blobs by offset.
369376
/// </summary>
370-
/// <seealso cref="System.Collections.Generic.IComparer&lt;MemoryManager.Core.BlobEntry&gt;" />
371377
private struct BlobOffsetComparer : IComparer<BlobEntry>
372378
{
373-
/// <inheritdoc />
374379
public int Compare(BlobEntry x, BlobEntry y)
375380
{
376381
return x.Offset.CompareTo(y.Offset);

0 commit comments

Comments
 (0)