From fc517142ded1165c5dccf316d07626e378831d5b Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 4 Apr 2024 05:52:23 +0100 Subject: [PATCH 1/3] Further optimize SlottedArray --- src/Paprika/Data/SlottedArray.cs | 107 +++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 19 deletions(-) diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index 4f3f1ad8..cdd03854 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -1,10 +1,8 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; using Paprika.Store; using Paprika.Utils; @@ -24,12 +22,24 @@ namespace Paprika.Data; public readonly ref struct SlottedArray { private readonly ref Header _header; - private readonly Span _data; + private readonly int _dataLength; public SlottedArray(Span buffer) { _header = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); - _data = buffer.Slice(Header.Size); + _dataLength = buffer.Length - Header.Size; + + if (_dataLength < 0) + { + ThrowBufferTooSmall(); + } + + [DoesNotReturn] + [StackTraceHidden] + static void ThrowBufferTooSmall() + { + throw new ArgumentOutOfRangeException("The buffer is too small"); + } } private readonly ref Slot this[int index] @@ -37,12 +47,12 @@ private readonly ref Slot this[int index] get { var offset = index * Slot.Size; - if (offset >= _data.Length - Slot.Size) + if (offset >= _dataLength - Slot.Size) { ThrowIndexOutOfRangeException(); } - return ref Unsafe.As(ref Unsafe.Add(ref MemoryMarshal.GetReference(_data), offset)); + return ref Unsafe.As(ref Unsafe.Add(ref Unsafe.As(ref _header), Header.Size + offset)); [DoesNotReturn] [StackTraceHidden] @@ -53,6 +63,51 @@ static void ThrowIndexOutOfRangeException() } } + private Span AsUshortSpan(int length) + { + if ((uint)length > (uint)_dataLength) + { + ThrowLengthTooLong(length, _dataLength); + } + if ((length & 0b1) != 0) + { + NotUshortLength(length); + } + + return MemoryMarshal.CreateSpan(ref Unsafe.AddByteOffset(ref Unsafe.As(ref _header), Header.Size), length / 2); + + static void NotUshortLength(int length) + { + throw new ArgumentOutOfRangeException($"The length must be even, is {length}"); + } + } + + private Span AsSpan(int offset) + { + if ((uint)offset > (uint)_dataLength) + { + ThrowOffsetTooLong(offset); + } + + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref Unsafe.As(ref _header), Header.Size + offset), _dataLength - offset); + } + + private Span AsSpan(int offset, int length) + { + if ((uint)offset > (uint)_dataLength) + { + ThrowOffsetTooLong(offset); + } + + var maxLength = _dataLength - offset; + if ((uint)length > (uint)maxLength) + { + ThrowLengthTooLong(length, maxLength); + } + + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref Unsafe.As(ref _header), Header.Size + offset), length); + } + public bool TrySet(in NibblePath key, ReadOnlySpan data) { var hash = Slot.PrepareKey(key, out var preamble, out var trimmed); @@ -73,7 +128,7 @@ public bool TrySet(in NibblePath key, ReadOnlySpan data) // does not exist yet, calculate total memory needed var total = GetTotalSpaceRequired(preamble, trimmed, data); - if (_header.Taken + total + Slot.Size > _data.Length) + if (_header.Taken + total + Slot.Size > _dataLength) { if (_header.Deleted == 0) { @@ -85,7 +140,7 @@ public bool TrySet(in NibblePath key, ReadOnlySpan data) Deframent(); // re-evaluate again - if (_header.Taken + total + Slot.Size > _data.Length) + if (_header.Taken + total + Slot.Size > _dataLength) { // not enough memory return false; @@ -98,10 +153,10 @@ public bool TrySet(in NibblePath key, ReadOnlySpan data) // write slot slot.Hash = hash; slot.KeyPreamble = preamble; - slot.ItemAddress = (ushort)(_data.Length - _header.High - total); + slot.ItemAddress = (ushort)(_dataLength - _header.High - total); // write item: length_key, key, data - var dest = _data.Slice(slot.ItemAddress, total); + var dest = AsSpan(slot.ItemAddress, total); if (HasKeyBytes(preamble)) { @@ -125,8 +180,8 @@ public bool TrySet(in NibblePath key, ReadOnlySpan data) /// public int Count => _header.Low / Slot.Size; - public int CapacityLeft => _data.Length - _header.Taken; - public int CapacityTotal => _data.Length; + public int CapacityLeft => _dataLength - _header.Taken; + public int CapacityTotal => _dataLength; public Enumerator EnumerateAll() => new(this); @@ -298,7 +353,7 @@ private void DeleteImpl(int index) private void Deframent() { // As data were fitting before, the will fit after so all the checks can be skipped - var size = Header.Size + _data.Length; + var size = Header.Size + _dataLength; var array = ArrayPool.Shared.Rent(size); var span = array.AsSpan(0, size); @@ -316,8 +371,8 @@ private void Deframent() ref var copyTo = ref copy[copy._header.Low / Slot.Size]; // copy raw, no decoding - var high = (ushort)(copy._data.Length - copy._header.High - fromSpan.Length); - fromSpan.CopyTo(copy._data.Slice(high)); + var high = (ushort)(copy._dataLength - copy._header.High - fromSpan.Length); + fromSpan.CopyTo(copy.AsSpan(high)); copyTo.Hash = copyFrom.Hash; copyTo.ItemAddress = high; @@ -329,7 +384,7 @@ private void Deframent() } // finalize by coping over to this - var raw = MemoryMarshal.CreateSpan(ref Unsafe.As(ref _header), Header.Size + _data.Length); + var raw = MemoryMarshal.CreateSpan(ref Unsafe.As(ref _header), Header.Size + _dataLength); span.CopyTo(raw); ArrayPool.Shared.Return(array); @@ -386,7 +441,7 @@ private bool TryGetImpl(in NibblePath key, ushort hash, byte preamble, out Span< // if the found index is odd -> found a slot to be queried const int notFound = -1; - var span = MemoryMarshal.Cast(_data.Slice(0, to)); + var span = AsUshortSpan(to); var offset = 0; int index = span.IndexOf(hash); @@ -459,10 +514,10 @@ private Span GetSlotPayload(ref Slot slot) // assert whether the slot has a previous, if not use data.length var previousSlotAddress = Unsafe.IsAddressLessThan(ref this[0], ref slot) ? Unsafe.Add(ref slot, -1).ItemAddress - : _data.Length; + : _dataLength; var length = previousSlotAddress - slot.ItemAddress; - return _data.Slice(slot.ItemAddress, length); + return AsSpan(slot.ItemAddress, length); } /// @@ -637,6 +692,20 @@ public static NibblePath UnPrepareKey(ReadOnlySpan input, ushort hash, byt } } + [DoesNotReturn] + [StackTraceHidden] + private void ThrowOffsetTooLong(int offset) + { + throw new ArgumentOutOfRangeException($"Offset {offset} is longer than dataLength {_dataLength}"); + } + + [DoesNotReturn] + [StackTraceHidden] + private static void ThrowLengthTooLong(int length, int maxLength) + { + throw new ArgumentOutOfRangeException($"Length {length} is longer than remaining length {maxLength}"); + } + public override string ToString() => $"{nameof(Count)}: {Count}, {nameof(CapacityLeft)}: {CapacityLeft}"; [StructLayout(LayoutKind.Sequential, Pack = sizeof(byte), Size = Size)] From 9e2508f2271a918cb353fe2957e0c09cb761c421 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 4 Apr 2024 05:56:48 +0100 Subject: [PATCH 2/3] Error also on negative indexes --- src/Paprika/Data/SlottedArray.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index cdd03854..7512d7d4 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -47,7 +47,7 @@ private readonly ref Slot this[int index] get { var offset = index * Slot.Size; - if (offset >= _dataLength - Slot.Size) + if ((uint)offset >= (uint)(_dataLength - Slot.Size)) { ThrowIndexOutOfRangeException(); } From 8199f0c6885e19fdbb616fb68569f20f1fedc6ed Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 4 Apr 2024 06:11:16 +0100 Subject: [PATCH 3/3] Clarity of argument --- src/Paprika/Data/SlottedArray.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index 7512d7d4..4a0ea663 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -441,7 +441,7 @@ private bool TryGetImpl(in NibblePath key, ushort hash, byte preamble, out Span< // if the found index is odd -> found a slot to be queried const int notFound = -1; - var span = AsUshortSpan(to); + var span = AsUshortSpan(length: to); var offset = 0; int index = span.IndexOf(hash);