diff --git a/src/Paprika.Benchmarks/SlottedArrayBenchmarks.cs b/src/Paprika.Benchmarks/SlottedArrayBenchmarks.cs index 7df1b26e..9311d608 100644 --- a/src/Paprika.Benchmarks/SlottedArrayBenchmarks.cs +++ b/src/Paprika.Benchmarks/SlottedArrayBenchmarks.cs @@ -11,6 +11,8 @@ public unsafe class SlottedArrayBenchmarks { private const int KeyCount = 97; + private const int Even = 0; + private const int BytesPerKey = 3; // 3 repeated bytes allow to cut off the first nibble and still have a unique key. Also, allow storing some key leftover @@ -44,7 +46,7 @@ public SlottedArrayBenchmarks() _map = AllocAlignedPage(); Span value = stackalloc byte[1]; - var map = new SlottedArray(new Span(_map, Page.PageSize)); + var map = new SlottedArray(new Span(_map, Page.PageSize), Even); for (byte i = 0; i < KeyCount; i++) { value[0] = i; @@ -74,7 +76,7 @@ public SlottedArrayBenchmarks() _hashCollidingMap = AllocAlignedPage(); - var hashColliding = new SlottedArray(new Span(_hashCollidingMap, Page.PageSize)); + var hashColliding = new SlottedArray(new Span(_hashCollidingMap, Page.PageSize), Even); for (byte i = 0; i < HashCollidingKeyCount; i++) { value[0] = i; @@ -109,7 +111,7 @@ public SlottedArrayBenchmarks() [Arguments((byte)KeyCount - 1, false)] public int TryGet(byte index, bool odd) { - var map = new SlottedArray(new Span(_map, Page.PageSize)); + var map = new SlottedArray(new Span(_map, Page.PageSize), Even); var key = GetKey(index, odd); var count = 0; @@ -129,7 +131,7 @@ public int TryGet(byte index, bool odd) [Arguments((byte)31)] public int TryGet_With_Hash_Collisions(byte index) { - var map = new SlottedArray(new Span(_hashCollidingMap, Page.PageSize)); + var map = new SlottedArray(new Span(_hashCollidingMap, Page.PageSize), Even); var key = GetHashCollidingKey(index); var count = 0; @@ -173,7 +175,7 @@ public int Prepare_Key(int sliceFrom, int length) [Benchmark] public int EnumerateAll() { - var map = new SlottedArray(new Span(_map, Page.PageSize)); + var map = new SlottedArray(new Span(_map, Page.PageSize), Even); var length = 0; foreach (var item in map.EnumerateAll()) @@ -190,7 +192,7 @@ public int EnumerateAll() [Arguments((byte)1)] public int EnumerateNibble(byte nibble) { - var map = new SlottedArray(new Span(_map, Page.PageSize)); + var map = new SlottedArray(new Span(_map, Page.PageSize), Even); var length = 0; foreach (var item in map.EnumerateNibble(nibble)) diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Defragment_when_no_more_space.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Defragment_when_no_more_space.verified.bin index a063ea6a..89759250 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Defragment_when_no_more_space.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Defragment_when_no_more_space.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=0.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=0.verified.bin index d1dbe250..71e0dacb 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=0.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=0.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=1.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=1.verified.bin index ddde90ab..714e4777 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=1.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_all_odd=1.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=0.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=0.verified.bin index 24dec898..61082aca 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=0.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=0.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=1.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=1.verified.bin index 2d12aa55..c3fcd8cd 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=1.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=0_lengthCutOff=1.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=0.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=0.verified.bin index 47808fdf..c6a7f449 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=0.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=0.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=1.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=1.verified.bin index 4dea15c4..175d088b 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=1.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_long_key_oddStart=1_lengthCutOff=1.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=1.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=1.verified.bin index 40b75d68..dd435956 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=1.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=1.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=2.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=2.verified.bin index 40b75d68..dd435956 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=2.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=2.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=3.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=3.verified.bin index 40b75d68..dd435956 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=3.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=3.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=4.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=4.verified.bin index 40b75d68..dd435956 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=4.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Enumerate_nibble_nibble=4.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Delete_Get_AnotherSet.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Delete_Get_AnotherSet.verified.bin index 29c4a096..56d49c60 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Delete_Get_AnotherSet.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Delete_Get_AnotherSet.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Empty.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Empty.verified.bin index 4a81e4fa..4b0617f1 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Empty.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Set_Get_Empty.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_resize.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_resize.verified.bin index 224fc1f8..3747da49 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_resize.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_resize.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_situ.verified.bin b/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_situ.verified.bin index acdfae01..bfa2c530 100644 Binary files a/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_situ.verified.bin and b/src/Paprika.Tests/Data/SlottedArrayTests.Update_in_situ.verified.bin differ diff --git a/src/Paprika.Tests/Data/SlottedArrayTests.cs b/src/Paprika.Tests/Data/SlottedArrayTests.cs index fd6da290..01444058 100644 --- a/src/Paprika.Tests/Data/SlottedArrayTests.cs +++ b/src/Paprika.Tests/Data/SlottedArrayTests.cs @@ -7,14 +7,16 @@ namespace Paprika.Tests.Data; public class SlottedArrayTests { - private static ReadOnlySpan Data0 => new byte[] { 23 }; - private static ReadOnlySpan Data1 => new byte[] { 29, 31 }; + private static ReadOnlySpan Data0 => [23]; + private static ReadOnlySpan Data1 => [29, 31]; - private static ReadOnlySpan Data2 => new byte[] { 37, 39 }; + private static ReadOnlySpan Data2 => [37, 39]; - private static ReadOnlySpan Data3 => new byte[] { 31, 41 }; - private static ReadOnlySpan Data4 => new byte[] { 23, 24, 25 }; - private static ReadOnlySpan Data5 => new byte[] { 23, 24, 64 }; + private static ReadOnlySpan Data3 => [31, 41]; + private static ReadOnlySpan Data4 => [23, 24, 25]; + private static ReadOnlySpan Data5 => [23, 24, 64]; + + private const byte Even = 0; [Test] public Task Set_Get_Delete_Get_AnotherSet() @@ -22,7 +24,7 @@ public Task Set_Get_Delete_Get_AnotherSet() var key0 = Values.Key0.Span; Span span = stackalloc byte[SlottedArray.MinimalSizeWithNoData + key0.Length + Data0.Length]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); map.SetAssert(key0, Data0); @@ -40,10 +42,12 @@ public Task Set_Get_Delete_Get_AnotherSet() } [Test] - public Task Enumerate_all([Values(0, 1)] int odd) + public Task Enumerate_all([Values((byte)0, (byte)1)] byte odd) { + var isEven = odd == 0; + Span span = stackalloc byte[256]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, odd); var key0 = NibblePath.Empty; var key1 = NibblePath.FromKey([7]).SliceFrom(odd); @@ -51,13 +55,21 @@ public Task Enumerate_all([Values(0, 1)] int odd) var key3 = NibblePath.FromKey([7, 13, 31]).SliceFrom(odd); var key4 = NibblePath.FromKey([7, 13, 31, 41]).SliceFrom(odd); - map.SetAssert(key0, Data0); + if (isEven) + { + map.SetAssert(key0, Data0); + } + map.SetAssert(key1, Data1); map.SetAssert(key2, Data2); map.SetAssert(key3, Data3); map.SetAssert(key4, Data4); - map.GetAssert(key0, Data0); + if (isEven) + { + map.GetAssert(key0, Data0); + } + map.GetAssert(key1, Data1); map.GetAssert(key2, Data2); map.GetAssert(key3, Data3); @@ -65,9 +77,12 @@ public Task Enumerate_all([Values(0, 1)] int odd) using var e = map.EnumerateAll(); - e.MoveNext().Should().BeTrue(); - e.Current.Key.Equals(key0).Should().BeTrue(); - e.Current.RawData.SequenceEqual(Data0).Should().BeTrue(); + if (isEven) + { + e.MoveNext().Should().BeTrue(); + e.Current.Key.Equals(key0).Should().BeTrue(); + e.Current.RawData.SequenceEqual(Data0).Should().BeTrue(); + } e.MoveNext().Should().BeTrue(); e.Current.Key.Equals(key1).Should().BeTrue(); @@ -95,7 +110,7 @@ public Task Enumerate_all([Values(0, 1)] int odd) public Task Enumerate_nibble([Values(1, 2, 3, 4)] int nibble) { Span span = stackalloc byte[256]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); var key0 = NibblePath.Empty; var key1 = NibblePath.FromKey([0x1A]); @@ -152,7 +167,7 @@ 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 map = new SlottedArray(span, Even); var key0 = NibblePath.Empty; var key1 = NibblePath.FromKey([0x10 | nibble1]); @@ -201,10 +216,10 @@ public void Enumerate_2_nibbles([Values(1, 2, 3, 4)] int nibble0) } [Test] - public Task Enumerate_long_key([Values(0, 1)] int oddStart, [Values(0, 1)] int lengthCutOff) + public Task Enumerate_long_key([Values((byte)0, (byte)1)] byte oddStart, [Values(0, 1)] int lengthCutOff) { Span span = stackalloc byte[128]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, oddStart); var key = NibblePath.FromKey(Keccak.EmptyTreeHash).SliceFrom(oddStart) .SliceTo(NibblePath.KeccakNibbleCount - oddStart - lengthCutOff); @@ -230,7 +245,7 @@ public Task Set_Get_Empty() var key0 = Values.Key0.Span; Span span = stackalloc byte[128]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); var data = ReadOnlySpan.Empty; @@ -261,7 +276,7 @@ public Task Defragment_when_no_more_space() { // by trial and error, found the smallest value that will allow to put these two Span span = stackalloc byte[SlottedArray.MinimalSizeWithNoData + 88]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); var key0 = Values.Key0.Span; var key1 = Values.Key1.Span; @@ -289,7 +304,7 @@ public Task Update_in_situ() { // by trial and error, found the smallest value that will allow to put these two Span span = stackalloc byte[128]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); var key1 = Values.Key1.Span; @@ -309,7 +324,7 @@ public Task Update_in_resize() // Update the value, with the next one being bigger. Span span = stackalloc byte[SlottedArray.MinimalSizeWithNoData + key0.Length + Data0.Length]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); map.SetAssert(key0, Data0); map.SetAssert(key0, Data2); @@ -324,7 +339,7 @@ public Task Update_in_resize() public Task Small_keys_compression() { Span span = stackalloc byte[512]; - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); Span key = stackalloc byte[1]; Span value = stackalloc byte[2]; @@ -353,9 +368,8 @@ public Task Small_keys_compression() return Verify(span.ToArray()); } - [TestCase(0)] - [TestCase(1)] - public void Key_of_length_5(int odd) + [Test] + public void Key_of_length_5([Values((byte)0, (byte)1)] byte odd) { const int length = 5; @@ -364,9 +378,9 @@ public void Key_of_length_5(int odd) Span span = stackalloc byte[SlottedArray.MinimalSizeWithNoData + spaceForKey]; - var key = NibblePath.FromKey(stackalloc byte[] { 0x34, 0x5, 0x7A }, odd, length); + var key = NibblePath.FromKey([0x34, 0x5, 0x7A], odd, length); - var map = new SlottedArray(span); + var map = new SlottedArray(span, odd); var value = ReadOnlySpan.Empty; map.SetAssert(key, value); @@ -384,9 +398,9 @@ public void Key_of_length_6_even() Span span = stackalloc byte[SlottedArray.MinimalSizeWithNoData + spaceForKey]; // 0b10 is the prefix of the nibble that can be densely encoded on one byte. - var key = NibblePath.FromKey(stackalloc byte[] { 0x34, 0b1001_1101, 0x7A }, 0, length); + var key = NibblePath.FromKey([0x34, 0b1001_1101, 0x7A], 0, length); - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); var value = ReadOnlySpan.Empty; map.SetAssert(key, value); @@ -404,9 +418,9 @@ public void Key_of_length_6_odd() Span span = stackalloc byte[SlottedArray.MinimalSizeWithNoData + spaceForKey]; // 0b10 is the prefix of the nibble that can be densely encoded on one byte. For odd, first 3 are consumed to prepare. - var key = NibblePath.FromKey(stackalloc byte[] { 0x04, 0b1011_0010, 0xD9, 0x7A }, 0, length); + var key = NibblePath.FromKey([0x04, 0b1011_0010, 0xD9, 0x7A], 0, length); - var map = new SlottedArray(span); + var map = new SlottedArray(span, Even); var value = ReadOnlySpan.Empty; map.SetAssert(key, value); @@ -420,7 +434,7 @@ public void Breach_VectorSize_with_key_count() var random = new Random(seed); Span key = stackalloc byte[4]; - var map = new SlottedArray(new byte[3 * 1024]); + var map = new SlottedArray(new byte[3 * 1024], Even); const int count = 257; @@ -451,7 +465,7 @@ public void Set_Get_With_Specific_Lengths([Values(8, 16, 32, 64, 68, 72)] int co keys[i * keyLength + 1] = i; } - var map = new SlottedArray(new byte[3 * 1024]); + var map = new SlottedArray(new byte[3 * 1024], Even); for (var i = 0; i < count; i++) { @@ -476,8 +490,8 @@ public void Set_Get_With_Specific_Lengths([Values(8, 16, 32, 64, 68, 72)] int co [TestCase(new[] { 0, 1, 7 })] public void Remove_keys_from(int[] indexes) { - var toRemove = new SlottedArray(stackalloc byte[512]); - var map = new SlottedArray(stackalloc byte[512]); + var toRemove = new SlottedArray(stackalloc byte[512], Even); + var map = new SlottedArray(stackalloc byte[512], Even); var key1 = NibblePath.Parse("1"); var key2 = NibblePath.Parse("23"); @@ -543,8 +557,8 @@ public void Remove_keys_from(int[] indexes) [Test] public void Move_to_1() { - var original = new SlottedArray(stackalloc byte[256]); - var copy0 = new SlottedArray(stackalloc byte[256]); + var original = new SlottedArray(stackalloc byte[256], Even); + var copy0 = new SlottedArray(stackalloc byte[256], Even); var key1 = NibblePath.Parse("1"); var key2 = NibblePath.Parse("23"); @@ -585,8 +599,8 @@ public void Move_to_respects_tombstones() { const int size = 256; - var original = new SlottedArray(stackalloc byte[size]); - var copy0 = new SlottedArray(stackalloc byte[size]); + var original = new SlottedArray(stackalloc byte[size], Even); + var copy0 = new SlottedArray(stackalloc byte[size], Even); var key1 = NibblePath.Parse("1"); var key2 = NibblePath.Parse("23"); @@ -625,9 +639,9 @@ public void Move_to_respects_tombstones() [Test] public void Move_to_2() { - var original = new SlottedArray(stackalloc byte[256]); - var copy0 = new SlottedArray(stackalloc byte[256]); - var copy1 = new SlottedArray(stackalloc byte[256]); + var original = new SlottedArray(stackalloc byte[256], Even); + var copy0 = new SlottedArray(stackalloc byte[256], Even); + var copy1 = new SlottedArray(stackalloc byte[256], Even); var key0 = NibblePath.Empty; var key1 = NibblePath.Parse("1"); @@ -676,11 +690,11 @@ public void Move_to_2() [Test] public void Move_to_4() { - var original = new SlottedArray(stackalloc byte[256]); - var copy0 = new SlottedArray(stackalloc byte[256]); - var copy1 = new SlottedArray(stackalloc byte[256]); - var copy2 = new SlottedArray(stackalloc byte[256]); - var copy3 = new SlottedArray(stackalloc byte[256]); + var original = new SlottedArray(stackalloc byte[256], Even); + var copy0 = new SlottedArray(stackalloc byte[256], Even); + var copy1 = new SlottedArray(stackalloc byte[256], Even); + var copy2 = new SlottedArray(stackalloc byte[256], Even); + var copy3 = new SlottedArray(stackalloc byte[256], Even); var key0 = NibblePath.Empty; var key1 = NibblePath.Parse("1"); @@ -747,15 +761,15 @@ public void Move_to_4() [Test] public void Move_to_8() { - var original = new SlottedArray(stackalloc byte[512]); - var copy0 = new SlottedArray(stackalloc byte[128]); - var copy1 = new SlottedArray(stackalloc byte[128]); - var copy2 = new SlottedArray(stackalloc byte[128]); - var copy3 = new SlottedArray(stackalloc byte[128]); - var copy4 = new SlottedArray(stackalloc byte[128]); - var copy5 = new SlottedArray(stackalloc byte[128]); - var copy6 = new SlottedArray(stackalloc byte[128]); - var copy7 = new SlottedArray(stackalloc byte[128]); + var original = new SlottedArray(stackalloc byte[512], Even); + var copy0 = new SlottedArray(stackalloc byte[128], Even); + var copy1 = new SlottedArray(stackalloc byte[128], Even); + var copy2 = new SlottedArray(stackalloc byte[128], Even); + var copy3 = new SlottedArray(stackalloc byte[128], Even); + var copy4 = new SlottedArray(stackalloc byte[128], Even); + var copy5 = new SlottedArray(stackalloc byte[128], Even); + var copy6 = new SlottedArray(stackalloc byte[128], Even); + var copy7 = new SlottedArray(stackalloc byte[128], Even); var key0 = NibblePath.Empty; var key1 = NibblePath.Parse("1"); @@ -895,16 +909,7 @@ public void Prepare_UnPrepare(int sliceFrom, int length) { var key = NibblePath.FromKey(Keccak.EmptyTreeHash).Slice(sliceFrom, length); - // prepare - var hash = SlottedArray.PrepareKeyForTests(key, out var preamble, out var trimmed); - var written = trimmed.IsEmpty ? ReadOnlySpan.Empty : trimmed.WriteTo(stackalloc byte[33]); - - Span working = stackalloc byte[32]; - - var unprepared = SlottedArray.UnPrepareKeyForTests(hash, preamble, written, working, out var data); - - data.IsEmpty.Should().BeTrue(); - key.Equals(unprepared).Should().BeTrue(); + SlottedArray.PrepareUnPrepareForTests(key); } } diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index ef7a1d24..1aab5126 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -21,10 +21,10 @@ namespace Paprika.Data; /// public readonly ref struct SlottedArray /*: IClearable */ { - public const int Alignment = 8; public const int HeaderSize = Header.Size; private readonly ref Header _header; + private readonly byte _oddity; private readonly Span _data; private static readonly int VectorSize = @@ -36,8 +36,10 @@ namespace Paprika.Data; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int AlignToDoubleVectorSize(int count) => (count + (DoubleVectorSize - 1)) & -DoubleVectorSize; - public SlottedArray(Span buffer) + public SlottedArray(Span buffer, byte oddity) { + _oddity = oddity; + Debug.Assert(buffer.Length >= MinimalSizeWithNoData, $"The buffer should be reasonably big, more than {MinimalSizeWithNoData}"); @@ -75,20 +77,24 @@ private ref Slot GetSlotRef(int index) return ref Unsafe.Add(ref Unsafe.As(ref MemoryMarshal.GetReference(_data)), offset); } - public void Set(in NibblePath key, ReadOnlySpan data) - { - var succeeded = TrySet(key, data); - Debug.Assert(succeeded); - } - public bool TrySet(in NibblePath key, ReadOnlySpan data) { + AssertKey(key); + var hash = Slot.PrepareKey(key, out var preamble, out var trimmed); return TrySetImpl(hash, preamble, trimmed, data); } + [Conditional("DEBUG")] + private void AssertKey(in NibblePath key) + { + Debug.Assert(key.IsEmpty || key.Oddity == _oddity); + } + public void DeleteByPrefix(in NibblePath prefix) { + AssertKey(prefix); + if (prefix.Length == 0) { Delete(prefix); @@ -254,7 +260,7 @@ private void Build(Slot slot, out Item value) { var hash = _map.GetHashRef(_index); var span = _map.GetSlotPayload(_index, slot); - var key = Slot.UnPrepareKey(hash, slot.KeyPreamble, span, _bytes.Span, out var data); + var key = Slot.UnPrepareKey(hash, slot.KeyPreamble, _map._oddity, span, _bytes.Span, out var data); value = new Item(key, data, _index); } @@ -262,7 +268,9 @@ private void Build(Slot slot, out Item value) // a shortcut to not allocate, just copy the enumerator public readonly Enumerator GetEnumerator() => this; - public void Dispose() { } + public void Dispose() + { + } } public ref struct NibbleEnumerator @@ -297,7 +305,7 @@ public bool MoveNext() while (index < to && (slot.IsDeleted || slot.HasAtLeastOneNibble == false || - slot.GetNibble0(hash) != _nibble)) // filter out deleted + Slot.GetNibble0(hash, _map._oddity) != _nibble)) // filter out deleted { // move by 1 index += 1; @@ -320,14 +328,16 @@ public bool MoveNext() 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); + var key = Slot.UnPrepareKey(hash, slot.KeyPreamble, _map._oddity, 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 void Dispose() { } + public void Dispose() + { + } } public ref struct Nibble2Enumerator @@ -361,8 +371,8 @@ public bool MoveNext() var hash = _map.GetHashRef(index); while (index < to && - (slot.IsDeleted || slot.HasAtLeastTwoNibbles(hash, slot.KeyPreamble) == false || - slot.GetNibble0And1(hash) != _searched)) // filter out deleted + (slot.IsDeleted || slot.HasAtLeastTwoNibbles(hash, _map._oddity) == false || + Slot.GetNibble0And1(hash, _map._oddity) != _searched)) // filter out deleted { // move by 1 index += 1; @@ -385,14 +395,16 @@ public bool MoveNext() 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); + var key = Slot.UnPrepareKey(hash, slot.KeyPreamble, _map._oddity, span, _bytes.Span, out var data); value = new Item(key, data, _index); } // a shortcut to not allocate, just copy the enumerator public readonly Nibble2Enumerator GetEnumerator() => this; - public void Dispose() { } + public void Dispose() + { + } } /// @@ -419,7 +431,7 @@ public void MoveNonEmptyKeysTo(in MapSource destination, bool treatEmptyAsTombst if (slot.HasAtLeastOneNibble == false) continue; - var nibble = slot.GetNibble0(GetHashRef(i)); + var nibble = Slot.GetNibble0(GetHashRef(i), _oddity); ref readonly var map = ref MapSource.GetMap(destination, nibble); var payload = GetSlotPayload(i); @@ -511,7 +523,7 @@ public void GatherCountStats1Nibble(Span buckets) // extract only not deleted and these which have at least one nibble if (slot.IsDeleted == false && slot.HasAtLeastOneNibble) { - buckets[slot.GetNibble0(GetHashRef(i))] += 1; + buckets[Slot.GetNibble0(GetHashRef(i), _oddity)] += 1; } } } @@ -533,7 +545,7 @@ public void GatherSizeStats1Nibble(Span buckets) { const int hashSize = sizeof(ushort); var size = (ushort)(GetSlotPayload(i, slot).Length + Slot.Size + hashSize); - buckets[slot.GetNibble0(GetHashRef(i))] += size; + buckets[Slot.GetNibble0(GetHashRef(i), _oddity)] += size; } } } @@ -555,6 +567,11 @@ private static class KeyEncoding { private const byte SpecialCaseMask = 0b1000_0000; + // Special case, empty + private const byte PathLengthOf0 = 0; + private const byte ZeroNibbleValue = 0; + private const byte ZeroNibbleLength = 1; + // Special case, length 1 private const byte PathLengthOf1 = 1; private const byte SingleNibbleCaseMask = 0b1111_0000; @@ -565,7 +582,10 @@ private static class KeyEncoding // The rest of the path is not important. // It should allow to encode 3/4 of paths of length 2 on a single byte. private const byte DoubleEvenNibbleCaseByteMask = DoubleEvenNibbleCaseFirstNibbleMask << NibblePath.NibbleShift; - private const byte DoubleEvenNibbleCaseByteMaskValue = DoubleEvenNibbleCaseFirstNibbleMaskValue << NibblePath.NibbleShift; + + private const byte DoubleEvenNibbleCaseByteMaskValue = + DoubleEvenNibbleCaseFirstNibbleMaskValue << NibblePath.NibbleShift; + private const byte DoubleEvenNibbleCaseFirstNibbleMask = 0b1100; private const byte DoubleEvenNibbleCaseFirstNibbleMaskValue = 0b1000; private const byte DoubleEvenNibbleCaseByteCount = 1; @@ -580,8 +600,12 @@ public static int GetBytesCount(in NibblePath key) { return key.Length switch { + PathLengthOf0 => ZeroNibbleLength, PathLengthOf1 => SingleNibbleLength, - PathLengthOf2 => (key.Nibble0 & DoubleEvenNibbleCaseFirstNibbleMask) == DoubleEvenNibbleCaseFirstNibbleMaskValue ? DoubleEvenNibbleCaseByteCount : 2, + PathLengthOf2 => (key.Nibble0 & DoubleEvenNibbleCaseFirstNibbleMask) == + DoubleEvenNibbleCaseFirstNibbleMaskValue + ? DoubleEvenNibbleCaseByteCount + : 2, _ => key.RawSpanLength + KeyLengthLength }; } @@ -591,6 +615,13 @@ public static bool TryReadFrom(scoped in Span actual, in NibblePath key, o AssertEven(key); var first = actual[0]; + + if (first == 0 && key.Length == 0) + { + leftover = actual[ZeroNibbleLength..]; + return true; + } + if ((first & SpecialCaseMask) == SpecialCaseMask) { // check lengths first, then construct a value that combines the prefix and the first nibble @@ -620,6 +651,12 @@ public static ReadOnlySpan ReadFrom(scoped in ReadOnlySpan actual, o { var first = actual[0]; + if (first == 0) + { + key = NibblePath.Empty; + return actual[ZeroNibbleLength..]; + } + if ((first & SpecialCaseMask) == SpecialCaseMask) { if ((first & SingleNibbleCaseMask) == SingleNibbleCaseMask) @@ -664,13 +701,20 @@ public static Span Write(in NibblePath key, in Span destination) { AssertEven(key); + if (key.Length == PathLengthOf0) + { + destination[0] = ZeroNibbleValue; + return destination[ZeroNibbleLength..]; + } + if (key.Length == PathLengthOf1) { destination[0] = (byte)(SingleNibbleCaseMask | key.Nibble0); return destination[SingleNibbleLength..]; } - if (key.Length == PathLengthOf2 && (key.UnsafeSpan & DoubleEvenNibbleCaseByteMask) == DoubleEvenNibbleCaseByteMaskValue) + if (key.Length == PathLengthOf2 && + (key.UnsafeSpan & DoubleEvenNibbleCaseByteMask) == DoubleEvenNibbleCaseByteMaskValue) { destination[0] = key.UnsafeSpan; return destination[DoubleEvenNibbleCaseByteCount..]; @@ -698,6 +742,8 @@ private static void AssertEven(in NibblePath key) /// public bool Delete(in NibblePath key) { + AssertKey(key); + var hash = Slot.PrepareKey(key, out var preamble, out var trimmed); var index = TryGetImpl(trimmed, hash, preamble, out _); if (index != NotFound) @@ -823,6 +869,8 @@ private void CollectTombstones() public bool TryGet(scoped in NibblePath key, out ReadOnlySpan data) { + AssertKey(key); + var hash = Slot.PrepareKey(key, out byte preamble, out var trimmed); if (TryGetImpl(trimmed, hash, preamble, out var span) != NotFound) { @@ -1013,9 +1061,29 @@ private Span GetSlotPayload(int index, Slot slot) /// public static ushort HashForTests(in NibblePath key) => Slot.PrepareKey(key, out _, out _); - public static NibblePath UnPrepareKeyForTests(ushort hash, byte preamble, ReadOnlySpan input, + public static void PrepareUnPrepareForTests(in NibblePath key) + { + var hash = Slot.PrepareKey(key, out var preamble, out var trimmed); + + Span span = default; + + if (HasKeyBytes(preamble)) + { + span = new byte[KeyEncoding.GetBytesCount(trimmed)]; + var leftover = KeyEncoding.Write(trimmed, span); + Debug.Assert(leftover.Length == 0, "Should have nothing left"); + } + + var oddity = (byte)key.Oddity; + var unprepared = Slot.UnPrepareKey(hash, preamble, oddity, span, new byte[32], out var data); + + Debug.Assert(data.Length == 0, "Data should be empty"); + Debug.Assert(unprepared.Equals(key), "Keys are different"); + } + + public static NibblePath UnPrepareKeyForTests(ushort hash, byte preamble, byte oddity, ReadOnlySpan input, Span workingSet, out ReadOnlySpan data) => - Slot.UnPrepareKey(hash, preamble, input, workingSet, out data); + Slot.UnPrepareKey(hash, preamble, oddity, input, workingSet, out data); public static ushort PrepareKeyForTests(in NibblePath key, out byte preamble, out NibblePath trimmed) => Slot.PrepareKey(key, out preamble, out trimmed); @@ -1035,9 +1103,9 @@ private struct Slot public const int Size = 2; /// - /// The address currently allows to address 8kb of data + /// The address currently allows to address 16kb of data /// - public const int MaximumAddressableSize = 8 * 1024; + public const int MaximumAddressableSize = 16 * 1024; /// /// The addressing requires 13 bits [0-12] to address whole page, leaving 3 bits for other purposes. @@ -1068,7 +1136,7 @@ public void MarkAsDeleted() // Preamble uses all bits that AddressMask does not private const ushort KeyPreambleMask = unchecked((ushort)~AddressMask); - private const ushort KeyPreambleShift = 13; + private const ushort KeyPreambleShift = 14; // There are 3 bits left, where 0th bit is used to mark the oddity. // There are two more bits that allow encoding 4 values: @@ -1086,48 +1154,42 @@ public void MarkAsDeleted() private const byte KeyPreambleLength0 = 0b00; private const byte KeyPreambleLength3OrLess = 0b01; - private const byte KeyPreambleLength4 = 0b10; - private const byte KeyPreambleLength5OrMore = 0b11; - private const byte KeyPreambleOddBit = 0b001; // The bit used for odd-starting paths. - private const byte KeyPreambleDelete = KeyPreambleOddBit; // Empty cannot be odd, odd is used as deleted marker. - // 0b110, 0b111 are not used + private const byte KeyPreambleLength4OrMore = 0b10; + private const byte KeyPreambleDelete = 0b11; - public const byte KeyPreambleWithBytes = KeyPreambleLength5OrMore << KeyPreambleLengthShift; + public const byte KeyPreambleWithBytes = KeyPreambleLength4OrMore; - private const byte KeyPreambleLengthShift = 1; private const byte KeyPreambleMaxEncodedLength = 4; private const byte KeySlice = 2; private const int HashByteShift = 8; - public bool HasAtLeastOneNibble => (KeyPreamble >> KeyPreambleLengthShift) > KeyPreambleLength0; + public bool HasAtLeastOneNibble => KeyPreamble > KeyPreambleLength0; - public bool HasAtLeastTwoNibbles(ushort hash, byte preamble) + public bool HasAtLeastTwoNibbles(ushort hash, byte oddity) { - return (KeyPreamble >> KeyPreambleLengthShift) switch + return KeyPreamble switch { KeyPreambleLength0 => false, - KeyPreambleLength3OrLess => GetLengthOf123(hash, preamble & KeyPreambleOddBit) >= 2, + KeyPreambleLength3OrLess => GetLengthOf123(hash, oddity) >= 2, _ => true }; } - public byte GetNibble0(ushort hash) + public static byte GetNibble0(ushort hash, byte oddity) { // Bitwise. Shift by 12, unless it's odd. If odd, shift by 8. return (byte)(0x0F & (hash >> (3 * NibblePath.NibbleShift - - ((Raw >> KeyPreambleShift) & KeyPreambleOddBit) * + oddity * NibblePath.NibbleShift))); } - public byte GetNibble0And1(ushort hash) + public static byte GetNibble0And1(ushort hash, byte oddity) { - var odd = (Raw >> KeyPreambleShift) & KeyPreambleOddBit; - const int shift = NibblePath.NibbleShift; - var nibble0 = (byte)(0x0F & (hash >> (3 * shift - odd * shift))); - var nibble1 = (byte)(0x0F & (hash >> (2 * shift - odd * shift))); + var nibble0 = (byte)(0x0F & (hash >> (3 * shift - oddity * shift))); + var nibble1 = (byte)(0x0F & (hash >> (2 * shift - oddity * shift))); return CombineNibbles(nibble0, nibble1); } @@ -1161,7 +1223,7 @@ public static ushort PrepareKey(in NibblePath key, out byte preamble, out Nibble trimmed = NibblePath.Empty; var length = key.Length; - preamble = (byte)(key.Oddity | (KeyPreambleLength3OrLess << KeyPreambleLengthShift)); + preamble = KeyPreambleLength3OrLess; ref var b = ref key.UnsafeSpan; @@ -1211,22 +1273,21 @@ public static ushort PrepareKey(in NibblePath key, out byte preamble, out Nibble // length 4: case 8: // even - preamble = KeyPreambleLength4 << KeyPreambleLengthShift; + preamble = KeyPreambleLength4OrMore; hash = (ushort)((b << HashByteShift) + Unsafe.Add(ref b, 1)); break; case 9: // odd - preamble = KeyPreambleOddBit | (KeyPreambleLength4 << KeyPreambleLengthShift); + preamble = KeyPreambleLength4OrMore; hash = (ushort)( ((b & 0x0F) << HashByteShift) + // 0th Unsafe.Add(ref b, 1) + // 1th &2nd ((Unsafe.Add(ref b, 2) & 0xF0) << HashByteShift) // 3rd, encoded as the highest ); break; - // beyond 4 default: - preamble = (byte)(KeyPreambleWithBytes | key.Oddity); + preamble = KeyPreambleLength4OrMore; trimmed = key.Slice(KeySlice + key.Oddity, length - KeyPreambleMaxEncodedLength); Debug.Assert(trimmed.IsOdd == false, "Trimmed should be always even"); @@ -1259,12 +1320,9 @@ public static ushort PrepareKey(in NibblePath key, out byte preamble, out Nibble } [SkipLocalsInit] - public static NibblePath UnPrepareKey(ushort hash, byte preamble, ReadOnlySpan input, + public static NibblePath UnPrepareKey(ushort hash, byte preamble, byte oddity, ReadOnlySpan input, Span workingSet, out ReadOnlySpan data) { - var odd = preamble & KeyPreambleOddBit; - var lengthBits = preamble >> KeyPreambleLengthShift; - // Get directly reference, hash is big endian ref var b = ref MemoryMarshal.GetReference(workingSet); @@ -1280,7 +1338,7 @@ public static NibblePath UnPrepareKey(ushort hash, byte preamble, ReadOnlySpan> (HashByteShift + NibblePath.NibbleShift)) & 0x0F)); diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index e73128eb..c31efce0 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -128,7 +128,7 @@ private static void Set(DbAddress at, in NibblePath key, in ReadOnlySpan d Debug.Assert(batch.WasWritten(current)); ref var payload = ref Unsafe.AsRef(page.Payload); - var map = new SlottedArray(payload.DataSpan); + var map = new SlottedArray(payload.DataSpan, page.Header.LevelOddity); if (page.Header.Metadata == Modes.Fanout) { @@ -225,7 +225,7 @@ private static void Set(DbAddress at, in NibblePath key, in ReadOnlySpan d // 1. check if delete, then delete in both. // 2. flush some down // 3. retry set - var overflow = GetWritableOverflow(batch, ref payload); + var overflow = GetWritableOverflow(batch, ref payload, page.Header.Level); // Assert existence Debug.Assert(payload.Buckets[LeafMode.Bucket0].IsNull == false); @@ -275,7 +275,7 @@ private static void TurnToFanOut(DbAddress current, in MapSource.Of2 overflow, I var page = batch.GetAt(current); ref var payload = ref Unsafe.AsRef(page.Payload); - var map = new SlottedArray(payload.DataSpan); + var map = new SlottedArray(payload.DataSpan, page.Header.LevelOddity); // Register for reuse and clear immediately. The overflow is kept as a map in memory var overflowAddress0 = payload.Buckets[LeafMode.Bucket0]; @@ -344,14 +344,14 @@ private static void GatherStats(in SlottedArray map, Span stats) //map.GatherSizeStats1Nibble(stats); } - private static MapSource.Of2 GetWritableOverflow(IBatchContext batch, ref Payload payload) + private static MapSource.Of2 GetWritableOverflow(IBatchContext batch, ref Payload payload, byte level) { - var overflow0 = EnsureOverflow(batch, ref payload, LeafMode.Bucket0); - var overflow1 = EnsureOverflow(batch, ref payload, LeafMode.Bucket1); + var overflow0 = EnsureOverflow(batch, ref payload, LeafMode.Bucket0, level); + var overflow1 = EnsureOverflow(batch, ref payload, LeafMode.Bucket1, level); return new MapSource.Of2(overflow0, overflow1); - static SlottedArray EnsureOverflow(IBatchContext batch, ref Payload payload, int bucket) + static SlottedArray EnsureOverflow(IBatchContext batch, ref Payload payload, int bucket, byte level) { var addr = payload.Buckets[bucket]; @@ -364,6 +364,8 @@ static SlottedArray EnsureOverflow(IBatchContext batch, ref Payload payload, int overflowPage = batch.GetNewPage(out addr, clear: false); overflowPage.Header.PageType = PageType.LeafOverflow; + overflowPage.Header.Level = level; // same level to align maps + leafOverflowPage = new LeafOverflowPage(overflowPage); leafOverflowPage.Map.Clear(); } @@ -384,7 +386,7 @@ static SlottedArray EnsureOverflow(IBatchContext batch, ref Payload payload, int public void Clear() { - new SlottedArray(Data.DataSpan).Clear(); + new SlottedArray(Data.DataSpan, page.Header.LevelOddity).Clear(); Data.Buckets.Clear(); } @@ -565,7 +567,7 @@ private static bool TryGet(IReadOnlyBatchContext batch, scoped in NibblePath key } - public SlottedArray Map => new(Data.DataSpan); + public SlottedArray Map => new(Data.DataSpan, page.Header.LevelOddity); public int CapacityLeft => Map.CapacityLeft; diff --git a/src/Paprika/Store/LeafOverflowPage.cs b/src/Paprika/Store/LeafOverflowPage.cs index 5ec5ae1a..c5c59e71 100644 --- a/src/Paprika/Store/LeafOverflowPage.cs +++ b/src/Paprika/Store/LeafOverflowPage.cs @@ -32,7 +32,7 @@ private struct Payload public Span DataSpan => MemoryMarshal.CreateSpan(ref DataStart, Size); } - public SlottedArray Map => new(Data.DataSpan); + public SlottedArray Map => new(Data.DataSpan, page.Header.LevelOddity); public void Accept(ref NibblePath.Builder builder, IPageVisitor visitor, IPageResolver resolver, DbAddress addr) { diff --git a/src/Paprika/Store/Page.cs b/src/Paprika/Store/Page.cs index 6eeb9db6..abe0c479 100644 --- a/src/Paprika/Store/Page.cs +++ b/src/Paprika/Store/Page.cs @@ -84,6 +84,8 @@ public struct PageHeader /// public byte Level; + public byte LevelOddity => (byte)(Level & 1); + public byte Metadata; } diff --git a/src/Paprika/Store/StateRootPage.cs b/src/Paprika/Store/StateRootPage.cs index 86681d90..9bc7ebce 100644 --- a/src/Paprika/Store/StateRootPage.cs +++ b/src/Paprika/Store/StateRootPage.cs @@ -32,7 +32,7 @@ public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat if (key.Length < ConsumedNibbles) { - var map = new SlottedArray(Data.DataSpan); + var map = new SlottedArray(Data.DataSpan, page.Header.LevelOddity); var isDelete = data.IsEmpty; if (isDelete) { @@ -129,7 +129,7 @@ public bool TryGet(IReadOnlyBatchContext batch, scoped in NibblePath key, out Re { if (key.Length < ConsumedNibbles) { - return new SlottedArray(Data.DataSpan).TryGet(key, out result); + return new SlottedArray(Data.DataSpan, page.Header.LevelOddity).TryGet(key, out result); } var index = GetIndex(key);