Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further optimize SlottedArray #301

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 88 additions & 19 deletions src/Paprika/Data/SlottedArray.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -24,25 +22,37 @@ namespace Paprika.Data;
public readonly ref struct SlottedArray
{
private readonly ref Header _header;
private readonly Span<byte> _data;
private readonly int _dataLength;

public SlottedArray(Span<byte> buffer)
{
_header = ref Unsafe.As<byte, Header>(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]
{
get
{
var offset = index * Slot.Size;
if (offset >= _data.Length - Slot.Size)
if ((uint)offset >= (uint)(_dataLength - Slot.Size))
{
ThrowIndexOutOfRangeException();
}

return ref Unsafe.As<byte, Slot>(ref Unsafe.Add(ref MemoryMarshal.GetReference(_data), offset));
return ref Unsafe.As<byte, Slot>(ref Unsafe.Add(ref Unsafe.As<Header, byte>(ref _header), Header.Size + offset));

[DoesNotReturn]
[StackTraceHidden]
Expand All @@ -53,6 +63,51 @@ static void ThrowIndexOutOfRangeException()
}
}

private Span<ushort> 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<Header, ushort>(ref _header), Header.Size), length / 2);

static void NotUshortLength(int length)
{
throw new ArgumentOutOfRangeException($"The length must be even, is {length}");
}
}

private Span<byte> AsSpan(int offset)
{
if ((uint)offset > (uint)_dataLength)
{
ThrowOffsetTooLong(offset);
}

return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref Unsafe.As<Header, byte>(ref _header), Header.Size + offset), _dataLength - offset);
}

private Span<byte> 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<Header, byte>(ref _header), Header.Size + offset), length);
}

public bool TrySet(in NibblePath key, ReadOnlySpan<byte> data)
{
var hash = Slot.PrepareKey(key, out var preamble, out var trimmed);
Expand All @@ -73,7 +128,7 @@ public bool TrySet(in NibblePath key, ReadOnlySpan<byte> 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)
{
Expand All @@ -85,7 +140,7 @@ public bool TrySet(in NibblePath key, ReadOnlySpan<byte> 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;
Expand All @@ -98,10 +153,10 @@ public bool TrySet(in NibblePath key, ReadOnlySpan<byte> 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))
{
Expand All @@ -125,8 +180,8 @@ public bool TrySet(in NibblePath key, ReadOnlySpan<byte> data)
/// </summary>
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);
Expand Down Expand Up @@ -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<byte>.Shared.Rent(size);
var span = array.AsSpan(0, size);

Expand All @@ -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;
Expand All @@ -329,7 +384,7 @@ private void Deframent()
}

// finalize by coping over to this
var raw = MemoryMarshal.CreateSpan(ref Unsafe.As<Header, byte>(ref _header), Header.Size + _data.Length);
var raw = MemoryMarshal.CreateSpan(ref Unsafe.As<Header, byte>(ref _header), Header.Size + _dataLength);
span.CopyTo(raw);

ArrayPool<byte>.Shared.Return(array);
Expand Down Expand Up @@ -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<byte, ushort>(_data.Slice(0, to));
var span = AsUshortSpan(length: to);

var offset = 0;
int index = span.IndexOf(hash);
Expand Down Expand Up @@ -459,10 +514,10 @@ private Span<byte> 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);
}

/// <summary>
Expand Down Expand Up @@ -648,6 +703,20 @@ public static NibblePath UnPrepareKey(ReadOnlySpan<byte> 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)]
Expand Down
Loading