diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.cs b/src/Paprika.Tests/Data/SlottedArrayTests.cs index d4a40772..fd6da290 100644 --- a/src/Paprika.Tests/Data/SlottedArrayTests.cs +++ b/src/Paprika.Tests/Data/SlottedArrayTests.cs @@ -46,10 +46,10 @@ public Task Enumerate_all([Values(0, 1)] int odd) var map = new SlottedArray(span); var key0 = NibblePath.Empty; - var key1 = NibblePath.FromKey(stackalloc byte[1] { 7 }).SliceFrom(odd); - var key2 = NibblePath.FromKey(stackalloc byte[2] { 7, 13 }).SliceFrom(odd); - var key3 = NibblePath.FromKey(stackalloc byte[3] { 7, 13, 31 }).SliceFrom(odd); - var key4 = NibblePath.FromKey(stackalloc byte[4] { 7, 13, 31, 41 }).SliceFrom(odd); + var key1 = NibblePath.FromKey([7]).SliceFrom(odd); + var key2 = NibblePath.FromKey([7, 13]).SliceFrom(odd); + var key3 = NibblePath.FromKey([7, 13, 31]).SliceFrom(odd); + var key4 = NibblePath.FromKey([7, 13, 31, 41]).SliceFrom(odd); map.SetAssert(key0, Data0); map.SetAssert(key1, Data1); @@ -98,10 +98,10 @@ public Task Enumerate_nibble([Values(1, 2, 3, 4)] int nibble) var map = new SlottedArray(span); var key0 = NibblePath.Empty; - var key1 = NibblePath.FromKey(stackalloc byte[1] { 0x1A }); - var key2 = NibblePath.FromKey(stackalloc byte[2] { 0x2A, 13 }); - var key3 = NibblePath.FromKey(stackalloc byte[3] { 0x3A, 13, 31 }); - var key4 = NibblePath.FromKey(stackalloc byte[4] { 0x4A, 13, 31, 41 }); + var key1 = NibblePath.FromKey([0x1A]); + var key2 = NibblePath.FromKey([0x2A, 13]); + var key3 = NibblePath.FromKey([0x3A, 13, 31]); + var key4 = NibblePath.FromKey([0x4A, 13, 31, 41]); map.SetAssert(key0, Data0); map.SetAssert(key1, Data1); @@ -146,6 +146,60 @@ public Task Enumerate_nibble([Values(1, 2, 3, 4)] int nibble) return Verify(span.ToArray()); } + [Test] + public void Enumerate_2_nibbles([Values(1, 2, 3, 4)] int nibble0) + { + const byte nibble1 = 0xA; + + Span span = stackalloc byte[256]; + var map = new SlottedArray(span); + + var key0 = NibblePath.Empty; + var key1 = NibblePath.FromKey([0x10 | nibble1]); + var key2 = NibblePath.FromKey([0x20 | nibble1, 13]); + var key3 = NibblePath.FromKey([0x30 | nibble1, 13, 31]); + var key4 = NibblePath.FromKey([0x40 | nibble1, 13, 31, 41]); + + map.SetAssert(key0, Data0); + map.SetAssert(key1, Data1); + map.SetAssert(key2, Data2); + map.SetAssert(key3, Data3); + map.SetAssert(key4, Data4); + + map.GetAssert(key0, Data0); + map.GetAssert(key1, Data1); + map.GetAssert(key2, Data2); + map.GetAssert(key3, Data3); + map.GetAssert(key4, Data4); + + var expected = nibble0 switch + { + 1 => key1, + 2 => key2, + 3 => key3, + 4 => key4, + _ => throw new Exception() + }; + + var data = nibble0 switch + { + 1 => Data1, + 2 => Data2, + 3 => Data3, + 4 => Data4, + _ => throw new Exception() + }; + + using var e = map.Enumerate2Nibbles((byte)nibble0, nibble1); + + e.MoveNext().Should().BeTrue(); + + e.Current.Key.Equals(expected).Should().BeTrue(); + e.Current.RawData.SequenceEqual(data).Should().BeTrue(); + + e.MoveNext().Should().BeFalse(); + } + [Test] public Task Enumerate_long_key([Values(0, 1)] int oddStart, [Values(0, 1)] int lengthCutOff) { diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index c5615b48..b41824be 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -101,10 +101,20 @@ public void DeleteByPrefix(in NibblePath prefix) Delete(item); } } + else if (prefix.Length == 2) + { + foreach (var item in Enumerate2Nibbles(prefix.FirstNibble, prefix.GetAt(1))) + { + if (item.Key.StartsWith(prefix)) + { + Delete(item); + } + } + } else { - // TODO: Try to optimize by filtering by hash better. Right now, the filter is only by the first nibble. - foreach (var item in EnumerateNibble(prefix.FirstNibble)) + // Filtering by 2 first nibbles should be sufficient to filter out a lot + foreach (var item in Enumerate2Nibbles(prefix.FirstNibble, prefix.GetAt(1))) { if (item.Key.StartsWith(prefix)) { @@ -191,6 +201,7 @@ private bool TrySetImpl(ushort hash, byte preamble, in NibblePath trimmed, ReadO public Enumerator EnumerateAll() => new(this); public NibbleEnumerator EnumerateNibble(byte nibble) => new(this, nibble); + public Nibble2Enumerator Enumerate2Nibbles(byte nibble0, byte nibble1) => new(this, nibble0, nibble1); [StructLayout(LayoutKind.Sequential, Pack = sizeof(byte), Size = Size)] private ref struct Chunk @@ -256,12 +267,10 @@ private void Build(Slot slot, out Item value) value = new Item(key, data, _index); } - public readonly void Dispose() - { - } - // a shortcut to not allocate, just copy the enumerator public readonly Enumerator GetEnumerator() => this; + + public void Dispose() { } } public ref struct NibbleEnumerator @@ -320,16 +329,78 @@ private void Build(Slot slot, ushort hash, out Item value) { var span = _map.GetSlotPayload(_index, slot); var key = Slot.UnPrepareKey(hash, slot.KeyPreamble, span, _bytes.Span, out var data); - value = new Item(key, data, _index); } - public readonly void Dispose() + // a shortcut to not allocate, just copy the enumerator + public readonly NibbleEnumerator GetEnumerator() => this; + + public void Dispose() { } + } + + public ref struct Nibble2Enumerator + { + /// The map being enumerated. + private readonly SlottedArray _map; + + private readonly byte _searched; + + /// The next index to yield. + private int _index; + + private Chunk _bytes; + private Item _current; + + internal Nibble2Enumerator(SlottedArray map, byte nibble0, byte nibble1) + { + _map = map; + _searched = Slot.CombineNibbles(nibble0, nibble1); + _index = -1; + } + + /// Advances the enumerator to the next element of the span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var index = _index + 1; + var to = _map.Count; + + var slot = _map.GetSlotRef(index); + var hash = _map.GetHashRef(index); + + while (index < to && + (slot.IsDeleted || slot.HasAtLeastTwoNibbles(hash, slot.KeyPreamble) == false || + slot.GetNibble0And1(hash) != _searched)) // filter out deleted + { + // move by 1 + index += 1; + slot = _map.GetSlotRef(index); + hash = _map.GetHashRef(index); + } + + if (index < to) + { + _index = index; + Build(slot, hash, out _current); + return true; + } + + return false; + } + + public readonly Item Current => _current; + + private void Build(Slot slot, ushort hash, out Item value) { + var span = _map.GetSlotPayload(_index, slot); + var key = Slot.UnPrepareKey(hash, slot.KeyPreamble, span, _bytes.Span, out var data); + value = new Item(key, data, _index); } // a shortcut to not allocate, just copy the enumerator - public readonly NibbleEnumerator GetEnumerator() => this; + public readonly Nibble2Enumerator GetEnumerator() => this; + + public void Dispose() { } } /// @@ -1066,7 +1137,12 @@ public byte GetNibble0And1(ushort hash) var nibble0 = (byte)(0x0F & (hash >> (3 * shift - odd * shift))); var nibble1 = (byte)(0x0F & (hash >> (2 * shift - odd * shift))); - return (byte)((nibble0 << shift) + nibble1); + return CombineNibbles(nibble0, nibble1); + } + + public static byte CombineNibbles(byte nibble0, byte nibble1) + { + return (byte)((nibble0 << NibblePath.NibbleShift) + nibble1); } public byte KeyPreamble