Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions LiteDB.Tests/Internals/BufferWriter_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,42 @@ public void Buffer_Write_Numbers()
p += 16;
}

[Fact]
public void Buffer_Number_Crosses_Segment_Boundaries()
{
var data = new byte[32];
var source = new[]
{
new BufferSlice(data, 0, 2),
new BufferSlice(data, 2, 2),
new BufferSlice(data, 4, 2),
new BufferSlice(data, 6, 2),
new BufferSlice(data, 8, 2),
new BufferSlice(data, 10, 2),
new BufferSlice(data, 12, 2),
new BufferSlice(data, 14, 2),
new BufferSlice(data, 16, 16)
};

var expectedInt = 0x01020304;
var expectedLong = 0x0102030405060708L;
var expectedDouble = 123456789.987654321d;

using (var w = new BufferWriter(source))
{
w.Write(expectedInt);
w.Write(expectedLong);
w.Write(expectedDouble);
}

using (var r = new BufferReader(source))
{
r.ReadInt32().Should().Be(expectedInt);
r.ReadInt64().Should().Be(expectedLong);
r.ReadDouble().Should().Be(expectedDouble);
}
}

[Fact]
public void Buffer_Write_Types()
{
Expand Down
89 changes: 74 additions & 15 deletions LiteDB/Engine/Disk/Serializer/BufferReader.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using static LiteDB.Constants;
Expand All @@ -21,6 +22,8 @@ internal partial class BufferReader : IDisposable
private bool _isEOF = false;

private static readonly ArrayPool<byte> _bufferPool = ArrayPool<byte>.Shared;
private const int StackallocThreshold = 16;
private delegate T SpanValueReader<T>(ReadOnlySpan<byte> span);

/// <summary>
/// Current global cursor position
Expand Down Expand Up @@ -174,38 +177,91 @@ private bool TryReadCStringCurrentSegment(out string value)
#endregion

#region Read Numbers

private T ReadNumber<T>(Func<byte[], int, T> convert, int size)
private void ReadInto(Span<byte> destination)
{
var written = 0;

while (written < destination.Length)
{
if (_isEOF && _currentPosition == _current.Count)
{
break;
}

var bytesLeft = _current.Count - _currentPosition;
if (bytesLeft == 0)
{
this.MoveForward(0);
continue;
}

var bytesToCopy = Math.Min(destination.Length - written, bytesLeft);

new ReadOnlySpan<byte>(_current.Array, _current.Offset + _currentPosition, bytesToCopy)
.CopyTo(destination.Slice(written, bytesToCopy));

written += bytesToCopy;

this.MoveForward(bytesToCopy);
}

ENSURE(written == destination.Length, "current value must fit inside defined buffer");
}

private T ReadNumber<T>(SpanValueReader<T> convert, int size)
{
T value;

// if fits in current segment, use inner array - otherwise copy from multiples segments
if (_currentPosition + size <= _current.Count)
{
value = convert(_current.Array, _current.Offset + _currentPosition);
var span = new ReadOnlySpan<byte>(_current.Array, _current.Offset + _currentPosition, size);
value = convert(span);

this.MoveForward(size);
}
else
{
var buffer = _bufferPool.Rent(size);
if (size <= StackallocThreshold)
{
Span<byte> buffer = stackalloc byte[size];

this.Read(buffer, 0, size);
this.ReadInto(buffer);

value = convert(buffer, 0);
value = convert(buffer);
}
else
{
var buffer = _bufferPool.Rent(size);

_bufferPool.Return(buffer, true);
try
{
this.Read(buffer, 0, size);

value = convert(new ReadOnlySpan<byte>(buffer, 0, size));
}
finally
{
_bufferPool.Return(buffer, true);
}
}
}

return value;
}

public Int32 ReadInt32() => this.ReadNumber(BitConverter.ToInt32, 4);
public Int64 ReadInt64() => this.ReadNumber(BitConverter.ToInt64, 8);
public UInt16 ReadUInt16() => this.ReadNumber(BitConverter.ToUInt16, 2);
public UInt32 ReadUInt32() => this.ReadNumber(BitConverter.ToUInt32, 4);
public Single ReadSingle() => this.ReadNumber(BitConverter.ToSingle, 4);
public Double ReadDouble() => this.ReadNumber(BitConverter.ToDouble, 8);
private static unsafe float Int32BitsToSingle(int value)
{
// Unsafe re-interpretation keeps Engine serializer span-based without extra allocations.
return *(float*)&value;
}

public Int32 ReadInt32() => this.ReadNumber(static span => BinaryPrimitives.ReadInt32LittleEndian(span), 4);
public Int64 ReadInt64() => this.ReadNumber(static span => BinaryPrimitives.ReadInt64LittleEndian(span), 8);
public UInt16 ReadUInt16() => this.ReadNumber(static span => BinaryPrimitives.ReadUInt16LittleEndian(span), 2);
public UInt32 ReadUInt32() => this.ReadNumber(static span => BinaryPrimitives.ReadUInt32LittleEndian(span), 4);
public Single ReadSingle() => this.ReadNumber(static span => Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(span)), 4);
public Double ReadDouble() => this.ReadNumber(static span => BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(span)), 8);

public Decimal ReadDecimal()
{
Expand Down Expand Up @@ -562,4 +618,7 @@ public void Dispose()
_source?.Dispose();
}
}
}
}



90 changes: 76 additions & 14 deletions LiteDB/Engine/Disk/Serializer/BufferWriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using static LiteDB.Constants;

Expand All @@ -19,6 +20,8 @@ internal partial class BufferWriter : IDisposable
private bool _isEOF = false;

private static readonly ArrayPool<byte> _bufferPool = ArrayPool<byte>.Shared;
private const int StackallocThreshold = 16;
private delegate void SpanFiller(Span<byte> span);

/// <summary>
/// Current global cursor position
Expand Down Expand Up @@ -162,32 +165,87 @@ public void Consume()

#region Numbers

private void WriteNumber<T>(T value, Action<T, byte[], int> toBytes, int size)
private void WriteFrom(ReadOnlySpan<byte> source)
{
var written = 0;

while (written < source.Length)
{
if (_isEOF && _currentPosition == _current.Count)
{
break;
}

var bytesLeft = _current.Count - _currentPosition;
if (bytesLeft == 0)
{
this.MoveForward(0);
continue;
}

var bytesToCopy = Math.Min(source.Length - written, bytesLeft);

source.Slice(written, bytesToCopy)
.CopyTo(new Span<byte>(_current.Array, _current.Offset + _currentPosition, bytesToCopy));

written += bytesToCopy;

this.MoveForward(bytesToCopy);
}

ENSURE(written == source.Length, "current value must fit inside defined buffer");
}

private void WriteNumber(SpanFiller fill, int size)
{
if (_currentPosition + size <= _current.Count)
{
toBytes(value, _current.Array, _current.Offset + _currentPosition);
var span = new Span<byte>(_current.Array, _current.Offset + _currentPosition, size);
fill(span);

this.MoveForward(size);
}
else
{
var buffer = _bufferPool.Rent(size);

toBytes(value, buffer, 0);
if (size <= StackallocThreshold)
{
Span<byte> buffer = stackalloc byte[size];

this.Write(buffer, 0, size);
fill(buffer);

_bufferPool.Return(buffer, true);
this.WriteFrom(buffer);
}
else
{
var buffer = _bufferPool.Rent(size);

try
{
var span = new Span<byte>(buffer, 0, size);
fill(span);

this.Write(buffer, 0, size);
}
finally
{
_bufferPool.Return(buffer, true);
}
}
}
}

public void Write(Int32 value) => this.WriteNumber(value, BufferExtensions.ToBytes, 4);
public void Write(Int64 value) => this.WriteNumber(value, BufferExtensions.ToBytes, 8);
public void Write(UInt16 value) => this.WriteNumber(value, BufferExtensions.ToBytes, 2);
public void Write(UInt32 value) => this.WriteNumber(value, BufferExtensions.ToBytes, 4);
public void Write(Single value) => this.WriteNumber(value, BufferExtensions.ToBytes, 4);
public void Write(Double value) => this.WriteNumber(value, BufferExtensions.ToBytes, 8);
private static unsafe int SingleToInt32Bits(float value)
{
// Unsafe cast lets the Engine serializer stay allocation-free when writing floats.
return *(int*)&value;
}

public void Write(Int32 value) => this.WriteNumber(span => BinaryPrimitives.WriteInt32LittleEndian(span, value), 4);
public void Write(Int64 value) => this.WriteNumber(span => BinaryPrimitives.WriteInt64LittleEndian(span, value), 8);
public void Write(UInt16 value) => this.WriteNumber(span => BinaryPrimitives.WriteUInt16LittleEndian(span, value), 2);
public void Write(UInt32 value) => this.WriteNumber(span => BinaryPrimitives.WriteUInt32LittleEndian(span, value), 4);
public void Write(Single value) => this.WriteNumber(span => BinaryPrimitives.WriteInt32LittleEndian(span, SingleToInt32Bits(value)), 4);
public void Write(Double value) => this.WriteNumber(span => BinaryPrimitives.WriteInt64LittleEndian(span, BitConverter.DoubleToInt64Bits(value)), 8);

public void Write(Decimal value)
{
Expand Down Expand Up @@ -444,3 +502,7 @@ public void Dispose()
}
}
}




Loading