Skip to content

Commit edf4300

Browse files
Try to be clever and improve my SortedKvStore.
1 parent bb3ddb0 commit edf4300

2 files changed

Lines changed: 93 additions & 45 deletions

File tree

CommonExtendedObjectsTests/SortedKvStoreTests.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void CompactRemovesUnoccupied()
100100
store.Remove(3);
101101
store.Compact();
102102

103-
Assert.AreEqual(8, store.Count);
103+
Assert.AreEqual(8, store.OccupiedCount);
104104
Assert.IsFalse(store.TryGetValue(1, out _));
105105
}
106106

@@ -199,7 +199,7 @@ public void PerformanceRemoveCompact()
199199
sw.Stop();
200200

201201
Trace.WriteLine($"Compact after removing half: {sw.ElapsedMilliseconds} ms");
202-
Assert.AreEqual(ItemCount / 2, store.Count);
202+
Assert.AreEqual(ItemCount / 2, store.OccupiedCount);
203203
}
204204

205205
/// <summary>
@@ -215,7 +215,7 @@ public void InsertKeyIntoEmptyStoreShouldKeepArraysInSync()
215215
store.Add(1, 42); // Insert a single key-value pair with key = 1
216216

217217
// Assert
218-
Assert.AreEqual(1, store.Count, "Store should have one occupied entry.");
218+
Assert.AreEqual(1, store.OccupiedCount, "Store should have one occupied entry.");
219219

220220
var keys = store.Keys.ToArray();
221221
Assert.AreEqual(1, keys.Length, "Keys enumerable should contain one item.");
@@ -252,7 +252,7 @@ public void InsertKeysThatForceShiftShouldKeepArraysInSync()
252252
Trace.WriteLine(store.ToString());
253253

254254
// Assert
255-
Assert.AreEqual(2, store.Count, "Store should have two occupied entries.");
255+
Assert.AreEqual(2, store.OccupiedCount, "Store should have two occupied entries.");
256256

257257
var keys = store.Keys.ToArray();
258258
CollectionAssert.AreEqual(new[] { 1, 2 }, keys, "Keys should be sorted and aligned.");
@@ -306,7 +306,7 @@ public void InsertKeysStartingAtOne_ShouldKeepArraysAligned()
306306
store.Add(2, 200); // Insert out of order to force shift
307307

308308
// Validate count
309-
Assert.AreEqual(3, store.Count, "Count should be 3 after inserts.");
309+
Assert.AreEqual(3, store.OccupiedCount, "Count should be 3 after inserts.");
310310

311311
// Validate keys are sorted
312312
var keys = store.Keys.ToArray();
@@ -324,7 +324,7 @@ public void InsertKeysStartingAtOne_ShouldKeepArraysAligned()
324324
// Compact to physically remove unoccupied entries
325325
store.Compact();
326326

327-
Assert.AreEqual(2, store.Count, "Count should be 2 after removal and compact.");
327+
Assert.AreEqual(2, store.OccupiedCount, "Count should be 2 after removal and compact.");
328328

329329
keys = store.Keys.ToArray();
330330
CollectionAssert.AreEqual(new[] { 1, 3 }, keys, "Keys after removal should be 1 and 3.");
@@ -358,7 +358,7 @@ public void RemoveManyShouldUpdateCountCorrectly()
358358
{ 5, 500 }
359359
};
360360

361-
Assert.AreEqual(5, store.Count);
361+
Assert.AreEqual(5, store.OccupiedCount);
362362

363363
// Remove keys 2 and 4
364364
var keysToRemove = new int[] { 2, 4 };
@@ -377,7 +377,7 @@ public void RemoveManyShouldUpdateCountCorrectly()
377377
Assert.IsTrue(store.ContainsKey(5));
378378

379379
// THIS WILL FAIL IF Count IS NOT UPDATED
380-
Assert.AreEqual(5, store.Count, "Count should NOT be updated after RemoveMany");
380+
Assert.AreEqual(5, store.OccupiedCount, "Count should NOT be updated after RemoveMany");
381381
}
382382

383383
/// <summary>
@@ -396,7 +396,7 @@ public void RemoveManyWithUnsortedKeysRemovesCorrectly()
396396
}
397397

398398
// Confirm initial state
399-
Assert.AreEqual(keys.Length, store.Count);
399+
Assert.AreEqual(keys.Length, store.OccupiedCount);
400400
foreach (var key in keys)
401401
{
402402
Assert.IsTrue(store.ContainsKey(key), $"Should contain key {key} initially.");
@@ -440,7 +440,7 @@ public void RemoveAndCompactTest()
440440
};
441441

442442
Assert.IsTrue(store.ContainsKey(3));
443-
Assert.AreEqual(5, store.Count);
443+
Assert.AreEqual(5, store.OccupiedCount);
444444

445445
// Remove key 3 logically
446446
store.Remove(3);
@@ -449,7 +449,7 @@ public void RemoveAndCompactTest()
449449
Assert.IsFalse(store.ContainsKey(3));
450450

451451
// Count still includes all, including logically removed
452-
Assert.AreEqual(5, store.Count);
452+
Assert.AreEqual(5, store.OccupiedCount);
453453

454454
// Compact physically removes unoccupied entries
455455
store.Compact();
@@ -458,7 +458,7 @@ public void RemoveAndCompactTest()
458458
Assert.IsFalse(store.ContainsKey(3));
459459

460460
// Count updated to reflect physical removal
461-
Assert.AreEqual(4, store.Count);
461+
Assert.AreEqual(4, store.OccupiedCount);
462462

463463
// Dump internal state for debug (optional)
464464
Trace.WriteLine(store.ToString());

ExtendedSystemObjects/SortedKvStore.cs

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public sealed class SortedKvStore : IDisposable, IEnumerable<KeyValuePair<int, i
4444
/// </summary>
4545
private readonly UnmanagedIntArray _values;
4646

47+
private UnmanagedIntList _activeIndicesCache;
48+
private bool _activeIndicesDirty = true;
49+
4750
/// <summary>
4851
/// Initializes a new instance of the <see cref="SortedKvStore" /> class with a specified initial capacity.
4952
/// </summary>
@@ -53,20 +56,21 @@ public SortedKvStore(int initialCapacity = 16)
5356
_keys = new UnmanagedIntArray(initialCapacity);
5457
_values = new UnmanagedIntArray(initialCapacity);
5558
_occupied = new UnmanagedIntArray(initialCapacity);
59+
_activeIndicesCache = new UnmanagedIntList(initialCapacity);
5660
}
5761

5862
/// <summary>
5963
/// Gets the number of active (occupied) key-value pairs stored.
6064
/// </summary>
61-
public int Count { get; private set; }
65+
public int OccupiedCount { get; private set; }
6266

6367
/// <summary>
6468
/// Gets the free capacity.
6569
/// </summary>
6670
/// <value>
6771
/// The free capacity.
6872
/// </value>
69-
public int FreeCapacity => _keys.Capacity - Count;
73+
public int FreeCapacity => _keys.Capacity - OccupiedCount;
7074

7175
/// <summary>
7276
/// Gets an enumerable collection of all keys currently in the store.
@@ -78,7 +82,7 @@ public IEnumerable<int> Keys
7882
{
7983
get
8084
{
81-
for (var i = 0; i < Count; i++)
85+
for (var i = 0; i < OccupiedCount; i++)
8286
{
8387
if (_occupied[i] != 0)
8488
{
@@ -124,6 +128,7 @@ public void Dispose()
124128
_keys.Dispose();
125129
_values.Dispose();
126130
_occupied.Dispose();
131+
_activeIndicesCache.Dispose();
127132
}
128133

129134
/// <inheritdoc />
@@ -133,12 +138,9 @@ public void Dispose()
133138
/// <returns>All active elements as Key Value pair.</returns>
134139
public IEnumerator<KeyValuePair<int, int>> GetEnumerator()
135140
{
136-
for (var i = 0; i < Count; i++)
141+
foreach (var idx in GetActiveIndices())
137142
{
138-
if (_occupied[i] != 0)
139-
{
140-
yield return new KeyValuePair<int, int>(_keys[i], _values[i]);
141-
}
143+
yield return new KeyValuePair<int, int>(_keys[idx], _values[idx]);
142144
}
143145
}
144146

@@ -167,25 +169,28 @@ public void Add(int key, int value)
167169
{
168170
_occupied[idx] = 1;
169171
_values[idx] = value;
170-
Count++;
172+
OccupiedCount++;
171173
}
172174
else
173175
{
174-
_values[idx] = value;
176+
_values[idx] = value; // update
175177
}
176178

177179
return;
178180
}
179181

180182
idx = ~idx;
181183

184+
// Insert at physical index, so check raw capacity not OccupiedCount
182185
EnsureCapacity();
183186

184187
_keys.InsertAt(idx, key);
185188
_values.InsertAt(idx, value);
186189
_occupied.InsertAt(idx, 1);
187190

188-
Count++;
191+
OccupiedCount++;
192+
193+
_activeIndicesDirty = true;
189194
}
190195

191196
/// <summary>
@@ -212,10 +217,10 @@ public bool ContainsKey(int key)
212217
/// <returns><c>true</c> if the key was found; otherwise, <c>false</c>.</returns>
213218
public bool TryGetValue(int key, out int value)
214219
{
215-
int left = 0, right = Count - 1;
216-
var keysSpan = _keys.AsSpan()[..Count];
217-
var occupiedSpan = _occupied.AsSpan()[..Count];
218-
var valuesSpan = _values.AsSpan()[..Count];
220+
int left = 0, right = OccupiedCount - 1;
221+
var keysSpan = _keys.AsSpan()[.._keys.Length];
222+
var occupiedSpan = _occupied.AsSpan()[..OccupiedCount];
223+
var valuesSpan = _values.AsSpan()[..OccupiedCount];
219224

220225
while (left <= right)
221226
{
@@ -272,10 +277,12 @@ public bool TryRemove(int key, out int index)
272277
if (index >= 0 && _occupied[index] != 0)
273278
{
274279
_occupied[index] = 0;
280+
_activeIndicesDirty = true;
275281
return true;
276282
}
277283

278284
index = -1;
285+
279286
return false;
280287
}
281288

@@ -291,8 +298,8 @@ public void RemoveMany(ReadOnlySpan<int> keysToRemove)
291298
return;
292299
}
293300

294-
var occSpan = _occupied.AsSpan()[..Count];
295-
var keysSpan = _keys.AsSpan()[..Count];
301+
var occSpan = _occupied.AsSpan()[..OccupiedCount];
302+
var keysSpan = _keys.AsSpan()[..OccupiedCount];
296303

297304
// Optional: if keysToRemove is sorted, do a linear merge
298305
// This path is faster than HashSet-based if input is sorted and large
@@ -312,7 +319,7 @@ public void RemoveMany(ReadOnlySpan<int> keysToRemove)
312319
if (isSorted)
313320
{
314321
int i = 0, j = 0;
315-
while (i < Count && j < keysToRemove.Length)
322+
while (i < OccupiedCount && j < keysToRemove.Length)
316323
{
317324
var currentKey = keysSpan[i];
318325
var removeKey = keysToRemove[j];
@@ -328,7 +335,7 @@ public void RemoveMany(ReadOnlySpan<int> keysToRemove)
328335
else
329336
{
330337
occSpan[i] = 0;
331-
338+
_activeIndicesDirty = true;
332339
i++;
333340
j++;
334341
}
@@ -343,6 +350,7 @@ public void RemoveMany(ReadOnlySpan<int> keysToRemove)
343350
if (idx >= 0 && occSpan[idx] != 0)
344351
{
345352
occSpan[idx] = 0;
353+
_activeIndicesDirty = true;
346354
}
347355
}
348356
}
@@ -354,13 +362,13 @@ public void RemoveMany(ReadOnlySpan<int> keysToRemove)
354362
/// </summary>
355363
public void Compact()
356364
{
357-
var occSpan = _occupied.AsSpan()[..Count];
358-
var maxRemoved = Count; // worst case: all are removed
365+
var occSpan = _occupied.AsSpan()[..OccupiedCount];
366+
var maxRemoved = OccupiedCount; // worst case: all are removed
359367

360368
var rented = ArrayPool<int>.Shared.Rent(maxRemoved);
361369
var removedCount = 0;
362370

363-
for (var i = 0; i < Count; i++)
371+
for (var i = 0; i < OccupiedCount; i++)
364372
{
365373
if (occSpan[i] == 0)
366374
{
@@ -380,11 +388,17 @@ public void Compact()
380388
_values.RemoveMultiple(indicesToRemove);
381389
_occupied.RemoveMultiple(indicesToRemove);
382390

383-
Count -= removedCount;
391+
OccupiedCount -= removedCount;
384392

385393
ArrayPool<int>.Shared.Return(rented);
386394
}
387395

396+
public Span<int> AsSpan()
397+
{
398+
var list = GetActiveIndices();
399+
return list.AsSpan();
400+
}
401+
388402
/// <summary>
389403
/// Performs a binary search for the specified key.
390404
/// </summary>
@@ -394,9 +408,13 @@ public void Compact()
394408
/// </returns>
395409
public int BinarySearch(int key)
396410
{
397-
int left = 0, right = Count - 1;
398-
var keysSpan = _keys.AsSpan()[..Count];
399-
var occupiedSpan = _occupied.AsSpan()[..Count];
411+
//var list = GetActiveIndices();
412+
//return list.AsSpan().BinarySearch(key);
413+
414+
415+
int left = 0, right = OccupiedCount - 1;
416+
var keysSpan = _keys.AsSpan()[..OccupiedCount];
417+
var occupiedSpan = _occupied.AsSpan()[..OccupiedCount];
400418

401419
while (left <= right)
402420
{
@@ -440,7 +458,7 @@ public int BinarySearch(int key)
440458

441459
// Key not found; try to move left forward to next occupied slot for insertion index
442460
int insertionIndex = left;
443-
while (insertionIndex < Count && occupiedSpan[insertionIndex] == 0)
461+
while (insertionIndex < OccupiedCount && occupiedSpan[insertionIndex] == 0)
444462
insertionIndex++;
445463

446464
return ~insertionIndex;
@@ -454,7 +472,9 @@ public void Clear()
454472
_keys.Clear();
455473
_values.Clear();
456474
_occupied.Clear();
457-
Count = 0;
475+
_activeIndicesCache.Clear();
476+
_activeIndicesDirty = true;
477+
OccupiedCount = 0;
458478
}
459479

460480
/// <summary>
@@ -466,9 +486,9 @@ public void Clear()
466486
public override string ToString()
467487
{
468488
var sb = new System.Text.StringBuilder();
469-
sb.AppendLine($"Count: {Count}");
489+
sb.AppendLine($"Count: {OccupiedCount}");
470490

471-
for (int i = 0; i < Count; i++)
491+
for (int i = 0; i < OccupiedCount; i++)
472492
{
473493
sb.AppendLine($"Index {i}: Key={_keys[i]}, Value={_values[i]}, Occupied={_occupied[i]}");
474494
}
@@ -490,9 +510,37 @@ public override string ToString()
490510
private void EnsureCapacity()
491511
{
492512
// Delegate to IntArray.EnsureCapacity, using Count + 1 since we add one item
493-
_keys.EnsureCapacity(Count + 1);
494-
_values.EnsureCapacity(Count + 1);
495-
_occupied.EnsureCapacity(Count + 1);
513+
_keys.EnsureCapacity(OccupiedCount + 1);
514+
_values.EnsureCapacity(OccupiedCount + 1);
515+
_occupied.EnsureCapacity(OccupiedCount + 1);
516+
}
517+
518+
private UnmanagedIntList GetActiveIndices()
519+
{
520+
if (!_activeIndicesDirty && _activeIndicesCache != null)
521+
{
522+
return _activeIndicesCache!;
523+
}
524+
525+
GenerateActiveIndices();
526+
_activeIndicesDirty = false;
527+
528+
return _activeIndicesCache!;
529+
}
530+
531+
private void GenerateActiveIndices()
532+
{
533+
_activeIndicesCache?.Dispose();
534+
_activeIndicesCache = new UnmanagedIntList(OccupiedCount); // or Count if unsure
535+
536+
var occ = _occupied.AsSpan()[.._keys.Length];
537+
for (int i = 0; i < occ.Length; i++)
538+
{
539+
if (occ[i] != 0)
540+
{
541+
_activeIndicesCache.Add(i);
542+
}
543+
}
496544
}
497545
}
498546
}

0 commit comments

Comments
 (0)