diff --git a/.gitignore b/.gitignore
index dba7f98..8f28b64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ bin
artifacts/
results/
logs/
+BenchmarkDotNet.Artifacts/
lmdb/lmdb/
testrun/
buildlog
diff --git a/src/LightningDB.Benchmarks/ComparerBenchmarkBase.cs b/src/LightningDB.Benchmarks/ComparerBenchmarkBase.cs
new file mode 100644
index 0000000..9bf8125
--- /dev/null
+++ b/src/LightningDB.Benchmarks/ComparerBenchmarkBase.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using BenchmarkDotNet.Attributes;
+
+namespace LightningDB.Benchmarks;
+
+///
+/// Base class for comparer benchmarks with configurable database setup.
+/// Derived classes must add their own [ParamsSource] attribute for the Comparer property.
+///
+public abstract class ComparerBenchmarkBase
+{
+ private string _path;
+
+ protected LightningEnvironment Env { get; private set; }
+ protected LightningDatabase DB { get; private set; }
+
+ // Note: Derived classes must add [ParamsSource] attribute and override this property
+ public virtual ComparerDescriptor Comparer { get; set; }
+
+ [Params(100, 1000)]
+ public int OpsPerTransaction { get; set; }
+
+ [Params(64, 256)]
+ public int ValueSize { get; set; }
+
+ protected byte[] ValueBuffer { get; private set; }
+ protected KeyBatch KeyBuffers { get; private set; }
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ Console.WriteLine($"Global Setup Begin - Comparer: {Comparer.Name}");
+
+ _path = $"BenchmarkDir_{Guid.NewGuid():N}";
+ if (Directory.Exists(_path))
+ Directory.Delete(_path, true);
+
+ Env = new LightningEnvironment(_path) { MaxDatabases = 1 };
+ Env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ if (Comparer.Comparer != null)
+ config.CompareWith(Comparer.Comparer);
+
+ using (var tx = Env.BeginTransaction()) {
+ DB = tx.OpenDatabase(configuration: config);
+ tx.Commit();
+ }
+
+ ValueBuffer = new byte[ValueSize];
+ KeyBuffers = GenerateKeys();
+
+ RunSetup();
+
+ Console.WriteLine("Global Setup End");
+ }
+
+ protected virtual KeyBatch GenerateKeys()
+ => KeyBatch.Generate(OpsPerTransaction, KeyOrdering.Sequential);
+
+ protected virtual void RunSetup() { }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ Console.WriteLine("Global Cleanup Begin");
+
+ try {
+ DB?.Dispose();
+ Env?.Dispose();
+
+ if (Directory.Exists(_path))
+ Directory.Delete(_path, true);
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex.ToString());
+ }
+
+ Console.WriteLine("Global Cleanup End");
+ }
+}
diff --git a/src/LightningDB.Benchmarks/ComparerDescriptor.cs b/src/LightningDB.Benchmarks/ComparerDescriptor.cs
new file mode 100644
index 0000000..72152d0
--- /dev/null
+++ b/src/LightningDB.Benchmarks/ComparerDescriptor.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using LightningDB.Comparers;
+
+namespace LightningDB.Benchmarks;
+
+///
+/// Wraps a comparer for BenchmarkDotNet parameterization with friendly display names.
+///
+public readonly struct ComparerDescriptor
+{
+ public string Name { get; }
+ public IComparer Comparer { get; }
+
+ private ComparerDescriptor(string name, IComparer comparer)
+ {
+ Name = name;
+ Comparer = comparer;
+ }
+
+ public override string ToString() => Name;
+
+ ///
+ /// All available comparers including Default (null = LMDB native).
+ ///
+ public static IEnumerable All => new[]
+ {
+ new ComparerDescriptor("Default", null),
+ new ComparerDescriptor("Bitwise", BitwiseComparer.Instance),
+ new ComparerDescriptor("ReverseBitwise", ReverseBitwiseComparer.Instance),
+ new ComparerDescriptor("SignedInt", SignedIntegerComparer.Instance),
+ new ComparerDescriptor("ReverseSignedInt", ReverseSignedIntegerComparer.Instance),
+ new ComparerDescriptor("UnsignedInt", UnsignedIntegerComparer.Instance),
+ new ComparerDescriptor("ReverseUnsignedInt", ReverseUnsignedIntegerComparer.Instance),
+ new ComparerDescriptor("Utf8String", Utf8StringComparer.Instance),
+ new ComparerDescriptor("ReverseUtf8String", ReverseUtf8StringComparer.Instance),
+ new ComparerDescriptor("Length", LengthComparer.Instance),
+ new ComparerDescriptor("ReverseLength", ReverseLengthComparer.Instance),
+ new ComparerDescriptor("LengthOnly", LengthOnlyComparer.Instance),
+ new ComparerDescriptor("HashCode", HashCodeComparer.Instance),
+ new ComparerDescriptor("Guid", GuidComparer.Instance),
+ new ComparerDescriptor("ReverseGuid", ReverseGuidComparer.Instance),
+ };
+
+ ///
+ /// Integer comparers only (for focused integer key benchmarks).
+ ///
+ public static IEnumerable IntegerComparers => new[]
+ {
+ new ComparerDescriptor("Default", null),
+ new ComparerDescriptor("SignedInt", SignedIntegerComparer.Instance),
+ new ComparerDescriptor("ReverseSignedInt", ReverseSignedIntegerComparer.Instance),
+ new ComparerDescriptor("UnsignedInt", UnsignedIntegerComparer.Instance),
+ new ComparerDescriptor("ReverseUnsignedInt", ReverseUnsignedIntegerComparer.Instance),
+ };
+
+ ///
+ /// GUID comparers only (for focused 16-byte key benchmarks).
+ ///
+ public static IEnumerable GuidComparers => new[]
+ {
+ new ComparerDescriptor("Default", null),
+ new ComparerDescriptor("Bitwise", BitwiseComparer.Instance),
+ new ComparerDescriptor("Guid", GuidComparer.Instance),
+ new ComparerDescriptor("ReverseGuid", ReverseGuidComparer.Instance),
+ };
+}
diff --git a/src/LightningDB.Benchmarks/ComparerReadBenchmarks.cs b/src/LightningDB.Benchmarks/ComparerReadBenchmarks.cs
new file mode 100644
index 0000000..d78fd0b
--- /dev/null
+++ b/src/LightningDB.Benchmarks/ComparerReadBenchmarks.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+
+namespace LightningDB.Benchmarks;
+
+///
+/// Benchmarks read operations across all comparers.
+/// Reads use comparers for B-tree traversal during key lookups.
+///
+[MemoryDiagnoser]
+public class ComparerReadBenchmarks : ComparerBenchmarkBase
+{
+ [ParamsSource(nameof(AllComparers))]
+ public override ComparerDescriptor Comparer { get; set; }
+
+ public static IEnumerable AllComparers => ComparerDescriptor.All;
+
+ protected override void RunSetup()
+ {
+ // Pre-populate database for reads
+ using var tx = Env.BeginTransaction();
+ for (var i = 0; i < KeyBuffers.Count; i++)
+ tx.Put(DB, KeyBuffers[i], ValueBuffer);
+ tx.Commit();
+ }
+
+ [Benchmark]
+ public void Read()
+ {
+ using var tx = Env.BeginTransaction(TransactionBeginFlags.ReadOnly);
+
+ for (var i = 0; i < OpsPerTransaction; i++) {
+ _ = tx.Get(DB, KeyBuffers[i]);
+ }
+ }
+}
diff --git a/src/LightningDB.Benchmarks/ComparerWriteBenchmarks.cs b/src/LightningDB.Benchmarks/ComparerWriteBenchmarks.cs
new file mode 100644
index 0000000..49eb234
--- /dev/null
+++ b/src/LightningDB.Benchmarks/ComparerWriteBenchmarks.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+
+namespace LightningDB.Benchmarks;
+
+///
+/// Benchmarks write operations across all comparers.
+/// Write operations trigger B-tree insertions which invoke comparers frequently.
+///
+[MemoryDiagnoser]
+public class ComparerWriteBenchmarks : ComparerBenchmarkBase
+{
+ [ParamsSource(nameof(AllComparers))]
+ public override ComparerDescriptor Comparer { get; set; }
+
+ public static IEnumerable AllComparers => ComparerDescriptor.All;
+
+ [Benchmark]
+ public void Write()
+ {
+ using var tx = Env.BeginTransaction();
+
+ for (var i = 0; i < OpsPerTransaction; i++) {
+ tx.Put(DB, KeyBuffers[i], ValueBuffer);
+ }
+
+ tx.Commit();
+ }
+}
diff --git a/src/LightningDB.Benchmarks/GuidComparerBenchmarks.cs b/src/LightningDB.Benchmarks/GuidComparerBenchmarks.cs
new file mode 100644
index 0000000..416a0b8
--- /dev/null
+++ b/src/LightningDB.Benchmarks/GuidComparerBenchmarks.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using BenchmarkDotNet.Attributes;
+
+namespace LightningDB.Benchmarks;
+
+///
+/// Focused benchmarks for GUID comparers testing their optimized 16-byte path.
+/// GuidComparer uses two ulong comparisons with big-endian reads for byte ordering.
+///
+[MemoryDiagnoser]
+public class GuidComparerBenchmarks
+{
+ private string _path;
+ private LightningEnvironment _env;
+ private LightningDatabase _db;
+ private byte[][] _keys;
+ private byte[] _valueBuffer;
+
+ [ParamsSource(nameof(GuidComparers))]
+ public ComparerDescriptor Comparer { get; set; }
+
+ public static IEnumerable GuidComparers
+ => ComparerDescriptor.GuidComparers;
+
+ [Params(1000, 10000)]
+ public int OpsPerTransaction { get; set; }
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ Console.WriteLine($"Global Setup Begin - Comparer: {Comparer.Name}");
+
+ _path = $"GuidBenchDir_{Guid.NewGuid():N}";
+ if (Directory.Exists(_path))
+ Directory.Delete(_path, true);
+
+ _env = new LightningEnvironment(_path) { MaxDatabases = 1 };
+ _env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ if (Comparer.Comparer != null)
+ config.CompareWith(Comparer.Comparer);
+
+ using (var tx = _env.BeginTransaction()) {
+ _db = tx.OpenDatabase(configuration: config);
+ tx.Commit();
+ }
+
+ _valueBuffer = new byte[64];
+ _keys = GenerateGuidKeys(OpsPerTransaction);
+
+ Console.WriteLine("Global Setup End");
+ }
+
+ private static byte[][] GenerateGuidKeys(int count)
+ {
+ var keys = new byte[count][];
+
+ for (var i = 0; i < count; i++) {
+ keys[i] = Guid.NewGuid().ToByteArray();
+ }
+
+ return keys;
+ }
+
+ [Benchmark]
+ public void WriteGuids()
+ {
+ using var tx = _env.BeginTransaction();
+
+ for (var i = 0; i < OpsPerTransaction; i++) {
+ tx.Put(_db, _keys[i], _valueBuffer);
+ }
+
+ tx.Commit();
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ Console.WriteLine("Global Cleanup Begin");
+
+ try {
+ _db?.Dispose();
+ _env?.Dispose();
+
+ if (Directory.Exists(_path))
+ Directory.Delete(_path, true);
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex.ToString());
+ }
+
+ Console.WriteLine("Global Cleanup End");
+ }
+}
diff --git a/src/LightningDB.Benchmarks/IntegerComparerBenchmarks.cs b/src/LightningDB.Benchmarks/IntegerComparerBenchmarks.cs
new file mode 100644
index 0000000..01b4b41
--- /dev/null
+++ b/src/LightningDB.Benchmarks/IntegerComparerBenchmarks.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using BenchmarkDotNet.Attributes;
+
+namespace LightningDB.Benchmarks;
+
+///
+/// Focused benchmarks for integer comparers testing their optimized 4-byte and 8-byte paths.
+/// SignedIntegerComparer and UnsignedIntegerComparer have optimized fast paths for
+/// int/uint (4 bytes) and long/ulong (8 bytes) that use direct memory reads.
+///
+[MemoryDiagnoser]
+public class IntegerComparerBenchmarks
+{
+ private string _path;
+ private LightningEnvironment _env;
+ private LightningDatabase _db;
+ private byte[][] _keys;
+ private byte[] _valueBuffer;
+
+ [ParamsSource(nameof(IntegerComparers))]
+ public ComparerDescriptor Comparer { get; set; }
+
+ public static IEnumerable IntegerComparers
+ => ComparerDescriptor.IntegerComparers;
+
+ [Params(4, 8)]
+ public int KeySize { get; set; }
+
+ [Params(1000, 10000)]
+ public int OpsPerTransaction { get; set; }
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ Console.WriteLine($"Global Setup Begin - Comparer: {Comparer.Name}, KeySize: {KeySize}");
+
+ _path = $"IntBenchDir_{Guid.NewGuid():N}";
+ if (Directory.Exists(_path))
+ Directory.Delete(_path, true);
+
+ _env = new LightningEnvironment(_path) { MaxDatabases = 1 };
+ _env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ if (Comparer.Comparer != null)
+ config.CompareWith(Comparer.Comparer);
+
+ using (var tx = _env.BeginTransaction()) {
+ _db = tx.OpenDatabase(configuration: config);
+ tx.Commit();
+ }
+
+ _valueBuffer = new byte[64];
+ _keys = GenerateIntegerKeys(OpsPerTransaction, KeySize);
+
+ Console.WriteLine("Global Setup End");
+ }
+
+ private static byte[][] GenerateIntegerKeys(int count, int keySize)
+ {
+ var keys = new byte[count][];
+
+ for (var i = 0; i < count; i++) {
+ keys[i] = new byte[keySize];
+ if (keySize == 4)
+ MemoryMarshal.Write(keys[i], in i);
+ else // keySize == 8
+ MemoryMarshal.Write(keys[i], (long)i);
+ }
+
+ return keys;
+ }
+
+ [Benchmark]
+ public void WriteIntegers()
+ {
+ using var tx = _env.BeginTransaction();
+
+ for (var i = 0; i < OpsPerTransaction; i++) {
+ tx.Put(_db, _keys[i], _valueBuffer);
+ }
+
+ tx.Commit();
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ Console.WriteLine("Global Cleanup Begin");
+
+ try {
+ _db?.Dispose();
+ _env?.Dispose();
+
+ if (Directory.Exists(_path))
+ Directory.Delete(_path, true);
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex.ToString());
+ }
+
+ Console.WriteLine("Global Cleanup End");
+ }
+}
diff --git a/src/LightningDB.Benchmarks/KeyBatch.cs b/src/LightningDB.Benchmarks/KeyBatch.cs
index a1db633..29f29eb 100644
--- a/src/LightningDB.Benchmarks/KeyBatch.cs
+++ b/src/LightningDB.Benchmarks/KeyBatch.cs
@@ -11,7 +11,7 @@ public enum KeyOrdering
}
///
-/// A collection of 4 byte key arrays
+/// A collection of key arrays with configurable size
///
public class KeyBatch
{
@@ -28,16 +28,19 @@ private KeyBatch(byte[][] buffers)
public static KeyBatch Generate(int keyCount, KeyOrdering keyOrdering)
+ => Generate(keyCount, keyOrdering, keySize: 4);
+
+ public static KeyBatch Generate(int keyCount, KeyOrdering keyOrdering, int keySize)
{
var buffers = new byte[keyCount][];
switch (keyOrdering) {
case KeyOrdering.Sequential:
- PopulateSequential(buffers);
+ PopulateSequential(buffers, keySize);
break;
case KeyOrdering.Random:
- PopulateRandom(buffers);
+ PopulateRandom(buffers, keySize);
break;
default:
@@ -47,14 +50,14 @@ public static KeyBatch Generate(int keyCount, KeyOrdering keyOrdering)
return new KeyBatch(buffers);
}
- private static void PopulateSequential(byte[][] buffers)
+ private static void PopulateSequential(byte[][] buffers, int keySize)
{
for (var i = 0; i < buffers.Length; i++) {
- buffers[i] = CopyToArray(i);
+ buffers[i] = CopyToArray(i, keySize);
}
}
- private static void PopulateRandom(byte[][] buffers)
+ private static void PopulateRandom(byte[][] buffers, int keySize)
{
var random = new Random(0);
var seen = new HashSet(buffers.Length);
@@ -63,17 +66,20 @@ private static void PopulateRandom(byte[][] buffers)
while (i < buffers.Length) {
var keyValue = random.Next(0, buffers.Length);
- if (!seen.Add(keyValue))
+ if (!seen.Add(keyValue))
continue;//skip duplicates
-
- buffers[i++] = CopyToArray(keyValue);
+
+ buffers[i++] = CopyToArray(keyValue, keySize);
}
}
- private static byte[] CopyToArray(int keyValue)
+ private static byte[] CopyToArray(int keyValue, int keySize)
{
- var key = new byte[4];
- MemoryMarshal.Write(key, in keyValue);
+ var key = new byte[keySize];
+ if (keySize >= 8)
+ MemoryMarshal.Write(key, (long)keyValue);
+ else
+ MemoryMarshal.Write(key, in keyValue);
return key;
}
}
\ No newline at end of file
diff --git a/src/LightningDB.Benchmarks/Main.cs b/src/LightningDB.Benchmarks/Main.cs
index f2d89f3..9af3b6d 100644
--- a/src/LightningDB.Benchmarks/Main.cs
+++ b/src/LightningDB.Benchmarks/Main.cs
@@ -2,11 +2,14 @@
namespace LightningDB.Benchmarks;
-public static class Entry
+public static class Entry
{
public static void Main(string[] args)
{
- //BenchmarkRunner.Run();
- BenchmarkRunner.Run();
+ // Use BenchmarkSwitcher for flexible execution:
+ // dotnet run -c Release -- --filter "*ComparerWrite*"
+ // dotnet run -c Release -- --filter "*IntegerComparer*"
+ // dotnet run -c Release -- --list flat
+ BenchmarkSwitcher.FromAssembly(typeof(Entry).Assembly).Run(args);
}
}
\ No newline at end of file
diff --git a/src/LightningDB.Tests/ComparerTests.cs b/src/LightningDB.Tests/ComparerTests.cs
index 821afe2..6e16de8 100644
--- a/src/LightningDB.Tests/ComparerTests.cs
+++ b/src/LightningDB.Tests/ComparerTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Runtime.InteropServices;
using LightningDB.Comparers;
using Shouldly;
@@ -85,19 +84,19 @@ public void signed_integer_comparer_sorts_int32_with_negatives_first()
using var cursor = txn.CreateCursor(db);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-50);
+ cursor.GetCurrent().key.Read().ShouldBe(-50);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-10);
+ cursor.GetCurrent().key.Read().ShouldBe(-10);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0);
+ cursor.GetCurrent().key.Read().ShouldBe(0);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50);
+ cursor.GetCurrent().key.Read().ShouldBe(50);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(100);
+ cursor.GetCurrent().key.Read().ShouldBe(100);
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
}
@@ -120,13 +119,13 @@ public void signed_integer_comparer_sorts_int64_with_negatives_first()
using var cursor = txn.CreateCursor(db);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-10L);
+ cursor.GetCurrent().key.Read().ShouldBe(-10L);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0L);
+ cursor.GetCurrent().key.Read().ShouldBe(0L);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50L);
+ cursor.GetCurrent().key.Read().ShouldBe(50L);
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
}
@@ -149,13 +148,13 @@ public void reverse_signed_integer_comparer_sorts_descending()
using var cursor = txn.CreateCursor(db);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50);
+ cursor.GetCurrent().key.Read().ShouldBe(50);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0);
+ cursor.GetCurrent().key.Read().ShouldBe(0);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-10);
+ cursor.GetCurrent().key.Read().ShouldBe(-10);
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
}
@@ -179,16 +178,16 @@ public void unsigned_integer_comparer_sorts_uint32()
using var cursor = txn.CreateCursor(db);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0u);
+ cursor.GetCurrent().key.Read().ShouldBe(0u);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50u);
+ cursor.GetCurrent().key.Read().ShouldBe(50u);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(100u);
+ cursor.GetCurrent().key.Read().ShouldBe(100u);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(uint.MaxValue);
+ cursor.GetCurrent().key.Read().ShouldBe(uint.MaxValue);
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
}
@@ -211,13 +210,13 @@ public void unsigned_integer_comparer_sorts_uint64()
using var cursor = txn.CreateCursor(db);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0UL);
+ cursor.GetCurrent().key.Read().ShouldBe(0UL);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50UL);
+ cursor.GetCurrent().key.Read().ShouldBe(50UL);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(ulong.MaxValue);
+ cursor.GetCurrent().key.Read().ShouldBe(ulong.MaxValue);
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
}
@@ -240,13 +239,13 @@ public void reverse_unsigned_integer_comparer_sorts_descending()
using var cursor = txn.CreateCursor(db);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(100u);
+ cursor.GetCurrent().key.Read().ShouldBe(100u);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50u);
+ cursor.GetCurrent().key.Read().ShouldBe(50u);
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
- MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0u);
+ cursor.GetCurrent().key.Read().ShouldBe(0u);
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
}
@@ -462,4 +461,171 @@ public void reverse_bitwise_comparer_works_with_duplicate_values()
cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.NotFound);
}
+
+ public void guid_comparer_sorts_guids_by_byte_order()
+ {
+ using var env = CreateEnvironment();
+ env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ config.CompareWith(GuidComparer.Instance);
+
+ using var txn = env.BeginTransaction();
+ using var db = txn.OpenDatabase(configuration: config);
+
+ // Create GUIDs with known byte patterns for predictable ordering
+ // First byte differs: 0x01 < 0x02 < 0xFF
+ var guid1 = new Guid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var guid2 = new Guid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var guid3 = new Guid(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+
+ txn.Put(db, guid2.ToByteArray(), new byte[] { 2 });
+ txn.Put(db, guid3.ToByteArray(), new byte[] { 3 });
+ txn.Put(db, guid1.ToByteArray(), new byte[] { 1 });
+
+ using var cursor = txn.CreateCursor(db);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid1);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid2);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid3);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
+ }
+
+ public void guid_comparer_sorts_by_second_half_when_first_half_equal()
+ {
+ using var env = CreateEnvironment();
+ env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ config.CompareWith(GuidComparer.Instance);
+
+ using var txn = env.BeginTransaction();
+ using var db = txn.OpenDatabase(configuration: config);
+
+ // Same first 8 bytes, differ in second half (byte 8)
+ var guid1 = new Guid(new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var guid2 = new Guid(new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var guid3 = new Guid(new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+
+ txn.Put(db, guid3.ToByteArray(), new byte[] { 3 });
+ txn.Put(db, guid1.ToByteArray(), new byte[] { 1 });
+ txn.Put(db, guid2.ToByteArray(), new byte[] { 2 });
+
+ using var cursor = txn.CreateCursor(db);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid1);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid2);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid3);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
+ }
+
+ public void reverse_guid_comparer_sorts_guids_descending()
+ {
+ using var env = CreateEnvironment();
+ env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ config.CompareWith(ReverseGuidComparer.Instance);
+
+ using var txn = env.BeginTransaction();
+ using var db = txn.OpenDatabase(configuration: config);
+
+ var guid1 = new Guid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var guid2 = new Guid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var guid3 = new Guid(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+
+ txn.Put(db, guid1.ToByteArray(), new byte[] { 1 });
+ txn.Put(db, guid2.ToByteArray(), new byte[] { 2 });
+ txn.Put(db, guid3.ToByteArray(), new byte[] { 3 });
+
+ using var cursor = txn.CreateCursor(db);
+
+ // Reverse order: FF, 02, 01
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid3);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid2);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid1);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
+ }
+
+ public void guid_comparer_falls_back_for_non_16_byte_values()
+ {
+ using var env = CreateEnvironment();
+ env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
+ config.CompareWith(GuidComparer.Instance);
+
+ using var txn = env.BeginTransaction();
+ using var db = txn.OpenDatabase(configuration: config);
+
+ // Non-16-byte keys should still work via fallback to SequenceCompareTo
+ txn.Put(db, new byte[] { 0xFF, 0xFF }, new byte[] { 1 });
+ txn.Put(db, new byte[] { 0x00, 0x00 }, new byte[] { 2 });
+ txn.Put(db, new byte[] { 0x80, 0x80 }, new byte[] { 3 });
+
+ using var cursor = txn.CreateCursor(db);
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0x00, 0x00 });
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0x80, 0x80 });
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
+ cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0xFF, 0xFF });
+
+ cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
+ }
+
+ public void guid_comparer_works_with_duplicate_values()
+ {
+ using var env = CreateEnvironment();
+ env.Open();
+
+ var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create | DatabaseOpenFlags.DuplicatesSort };
+ config.FindDuplicatesWith(GuidComparer.Instance);
+
+ using var txn = env.BeginTransaction();
+ using var db = txn.OpenDatabase(configuration: config);
+
+ var key = new byte[] { 1 };
+ var val1 = new Guid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var val2 = new Guid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ var val3 = new Guid(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
+
+ txn.Put(db, key, val3.ToByteArray());
+ txn.Put(db, key, val1.ToByteArray());
+ txn.Put(db, key, val2.ToByteArray());
+
+ using var cursor = txn.CreateCursor(db);
+ cursor.SetKey(key);
+
+ new Guid(cursor.GetCurrent().value.CopyToNewArray()).ShouldBe(val1);
+
+ cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().value.CopyToNewArray()).ShouldBe(val2);
+
+ cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.Success);
+ new Guid(cursor.GetCurrent().value.CopyToNewArray()).ShouldBe(val3);
+
+ cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.NotFound);
+ }
}
diff --git a/src/LightningDB.Tests/CursorTests.cs b/src/LightningDB.Tests/CursorTests.cs
index 8675461..dc2d525 100644
--- a/src/LightningDB.Tests/CursorTests.cs
+++ b/src/LightningDB.Tests/CursorTests.cs
@@ -275,10 +275,10 @@ public void should_get_both_range()
var result = c.GetBothRange(key, values[1]);
result.ShouldBe(MDBResultCode.Success);
var current = c.GetCurrent();
- current.value.CopyToNewArray().ShouldBe(values[1]);
- }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
+ current.value.Read().ShouldBe(2);
+ }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
}
-
+
public void should_get_both_range_with_span()
{
using var env = CreateEnvironment();
@@ -290,10 +290,10 @@ public void should_get_both_range_with_span()
var result = c.GetBothRange(key, values[1].AsSpan());
result.ShouldBe(MDBResultCode.Success);
var current = c.GetCurrent();
- current.value.CopyToNewArray().ShouldBe(values[1]);
- }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
+ current.value.Read().ShouldBe(2);
+ }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
}
-
+
public void should_move_to_first_duplicate()
{
using var env = CreateEnvironment();
@@ -306,10 +306,10 @@ public void should_move_to_first_duplicate()
result.ShouldBe(MDBResultCode.Success);
var dupResult = c.FirstDuplicate();
dupResult.resultCode.ShouldBe(MDBResultCode.Success);
- dupResult.value.CopyToNewArray().ShouldBe(values[0]);
- }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
+ dupResult.value.Read().ShouldBe(1);
+ }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
}
-
+
public void should_move_to_last_duplicate()
{
using var env = CreateEnvironment();
@@ -321,8 +321,8 @@ public void should_move_to_last_duplicate()
c.Set(key);
var result = c.LastDuplicate();
result.resultCode.ShouldBe(MDBResultCode.Success);
- result.value.CopyToNewArray().ShouldBe(values[4]);
- }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
+ result.value.Read().ShouldBe(5);
+ }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
}
public void all_values_for_should_only_return_matching_key_values()
@@ -364,8 +364,8 @@ public void should_move_to_next_no_duplicate()
var values = PopulateMultipleCursorValues(c);
var result = c.NextNoDuplicate();
result.resultCode.ShouldBe(MDBResultCode.Success);
- result.value.CopyToNewArray().ShouldBe(values[0]);
- }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
+ result.value.Read().ShouldBe(1);
+ }, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
}
@@ -490,7 +490,7 @@ public void should_move_to_previous_duplicate()
result.resultCode.ShouldBe(MDBResultCode.Success);
// Verify we're at the second-to-last value
- result.value.CopyToNewArray().ShouldBe(values[3]); // Previous of the last (values[4])
+ result.value.Read().ShouldBe(4); // Previous of the last (values[4])
}, DatabaseOpenFlags.DuplicatesFixed | DatabaseOpenFlags.Create);
}
diff --git a/src/LightningDB.Tests/TransactionTests.cs b/src/LightningDB.Tests/TransactionTests.cs
index 7ba3ef6..5fde364 100644
--- a/src/LightningDB.Tests/TransactionTests.cs
+++ b/src/LightningDB.Tests/TransactionTests.cs
@@ -203,7 +203,7 @@ public void transaction_should_support_custom_comparer()
{
int Comparison(int l, int r) => l.CompareTo(r);
var options = new DatabaseConfiguration {Flags = DatabaseOpenFlags.Create};
- int CompareWith(MDBValue l, MDBValue r) => Comparison(BitConverter.ToInt32(l.CopyToNewArray(), 0), BitConverter.ToInt32(r.CopyToNewArray(), 0));
+ int CompareWith(MDBValue l, MDBValue r) => Comparison(l.Read(), r.Read());
options.CompareWith(Comparer.Create(new Comparison((Func)CompareWith)));
using var env = CreateEnvironment();
@@ -231,7 +231,7 @@ public void transaction_should_support_custom_comparer()
var order = 0;
(MDBResultCode, MDBValue, MDBValue) result;
while ((result = c.Next()).Item1 == MDBResultCode.Success)
- BitConverter.ToInt32(result.Item2.CopyToNewArray()).ShouldBe(keysSorted[order++]);
+ result.Item2.Read().ShouldBe(keysSorted[order++]);
}
}
@@ -243,7 +243,7 @@ public void transaction_should_support_custom_dup_sorter()
env.Open();
using var txn = env.BeginTransaction();
var options = new DatabaseConfiguration {Flags = DatabaseOpenFlags.Create | DatabaseOpenFlags.DuplicatesFixed};
- int CompareWith(MDBValue l, MDBValue r) => Comparison(BitConverter.ToInt32(l.CopyToNewArray(), 0), BitConverter.ToInt32(r.CopyToNewArray(), 0));
+ int CompareWith(MDBValue l, MDBValue r) => Comparison(l.Read(), r.Read());
options.FindDuplicatesWith(Comparer.Create(new Comparison((Func)CompareWith)));
using var db = txn.OpenDatabase(configuration: options, closeOnDispose: true);
@@ -260,7 +260,7 @@ public void transaction_should_support_custom_dup_sorter()
(MDBResultCode, MDBValue, MDBValue) result;
while ((result = c.Next()).Item1 == MDBResultCode.Success)
- BitConverter.ToInt32(result.Item3.CopyToNewArray()).ShouldBe(valuesSorted[order++]);
+ result.Item3.Read().ShouldBe(valuesSorted[order++]);
}
}
public void database_should_be_empty_after_truncate()
diff --git a/src/LightningDB/Comparers/GuidComparer.cs b/src/LightningDB/Comparers/GuidComparer.cs
new file mode 100644
index 0000000..af981c7
--- /dev/null
+++ b/src/LightningDB/Comparers/GuidComparer.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace LightningDB.Comparers;
+
+///
+/// Compares MDBValue instances as GUIDs using byte ordering (lexicographic).
+/// Optimized for 16-byte values using two ulong comparisons with early termination.
+/// Falls back to bitwise comparison for non-16-byte inputs.
+///
+///
+/// This comparer uses byte-level ordering (memcmp-style), NOT Guid.CompareTo() ordering.
+/// GUIDs are compared as raw byte sequences from first byte to last.
+///
+public sealed class GuidComparer : IComparer
+{
+ public static readonly GuidComparer Instance = new();
+
+ private GuidComparer() { }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Compare(MDBValue x, MDBValue y)
+ {
+ var left = x.AsSpan();
+ var right = y.AsSpan();
+
+ if (left.Length == 16 && right.Length == 16)
+ {
+ // Compare first 8 bytes (big-endian for correct byte ordering)
+ var leftHigh = BinaryPrimitives.ReadUInt64BigEndian(left);
+ var rightHigh = BinaryPrimitives.ReadUInt64BigEndian(right);
+
+ var cmp = leftHigh.CompareTo(rightHigh);
+ if (cmp != 0)
+ return cmp;
+
+ // Compare last 8 bytes
+ var leftLow = BinaryPrimitives.ReadUInt64BigEndian(left.Slice(8));
+ var rightLow = BinaryPrimitives.ReadUInt64BigEndian(right.Slice(8));
+
+ return leftLow.CompareTo(rightLow);
+ }
+
+ return left.SequenceCompareTo(right);
+ }
+}
diff --git a/src/LightningDB/Comparers/ReverseGuidComparer.cs b/src/LightningDB/Comparers/ReverseGuidComparer.cs
new file mode 100644
index 0000000..f210b66
--- /dev/null
+++ b/src/LightningDB/Comparers/ReverseGuidComparer.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace LightningDB.Comparers;
+
+///
+/// Compares MDBValue instances as GUIDs in reverse byte order (descending).
+/// Optimized for 16-byte values using two ulong comparisons with early termination.
+/// Falls back to reverse bitwise comparison for non-16-byte inputs.
+///
+///
+/// This comparer uses reverse byte-level ordering, NOT reverse Guid.CompareTo() ordering.
+/// GUIDs are compared as raw byte sequences from first byte to last, then reversed.
+///
+public sealed class ReverseGuidComparer : IComparer
+{
+ public static readonly ReverseGuidComparer Instance = new();
+
+ private ReverseGuidComparer() { }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Compare(MDBValue x, MDBValue y)
+ {
+ var left = x.AsSpan();
+ var right = y.AsSpan();
+
+ if (left.Length == 16 && right.Length == 16)
+ {
+ // Compare first 8 bytes (reversed: right vs left)
+ var leftHigh = BinaryPrimitives.ReadUInt64BigEndian(left);
+ var rightHigh = BinaryPrimitives.ReadUInt64BigEndian(right);
+
+ var cmp = rightHigh.CompareTo(leftHigh);
+ if (cmp != 0)
+ return cmp;
+
+ // Compare last 8 bytes (reversed)
+ var leftLow = BinaryPrimitives.ReadUInt64BigEndian(left.Slice(8));
+ var rightLow = BinaryPrimitives.ReadUInt64BigEndian(right.Slice(8));
+
+ return rightLow.CompareTo(leftLow);
+ }
+
+ return right.SequenceCompareTo(left);
+ }
+}
diff --git a/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs b/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs
index df1875f..8cf35a3 100644
--- a/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs
+++ b/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Runtime.InteropServices;
namespace LightningDB.Comparers;
@@ -20,10 +19,10 @@ public int Compare(MDBValue x, MDBValue y)
var right = y.AsSpan();
if (left.Length == 4 && right.Length == 4)
- return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left));
+ return y.Read().CompareTo(x.Read());
if (left.Length == 8 && right.Length == 8)
- return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left));
+ return y.Read().CompareTo(x.Read());
return right.SequenceCompareTo(left);
}
diff --git a/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs b/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs
index 755db65..3e13054 100644
--- a/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs
+++ b/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Runtime.InteropServices;
namespace LightningDB.Comparers;
@@ -20,10 +19,10 @@ public int Compare(MDBValue x, MDBValue y)
var right = y.AsSpan();
if (left.Length == 4 && right.Length == 4)
- return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left));
+ return y.Read().CompareTo(x.Read());
if (left.Length == 8 && right.Length == 8)
- return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left));
+ return y.Read().CompareTo(x.Read());
return right.SequenceCompareTo(left);
}
diff --git a/src/LightningDB/Comparers/SignedIntegerComparer.cs b/src/LightningDB/Comparers/SignedIntegerComparer.cs
index 2dcc74d..3bd686d 100644
--- a/src/LightningDB/Comparers/SignedIntegerComparer.cs
+++ b/src/LightningDB/Comparers/SignedIntegerComparer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Runtime.InteropServices;
namespace LightningDB.Comparers;
@@ -21,10 +20,10 @@ public int Compare(MDBValue x, MDBValue y)
var right = y.AsSpan();
if (left.Length == 4 && right.Length == 4)
- return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right));
+ return x.Read().CompareTo(y.Read());
if (left.Length == 8 && right.Length == 8)
- return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right));
+ return x.Read().CompareTo(y.Read());
return left.SequenceCompareTo(right);
}
diff --git a/src/LightningDB/Comparers/UnsignedIntegerComparer.cs b/src/LightningDB/Comparers/UnsignedIntegerComparer.cs
index 546d5ee..f575cf3 100644
--- a/src/LightningDB/Comparers/UnsignedIntegerComparer.cs
+++ b/src/LightningDB/Comparers/UnsignedIntegerComparer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Runtime.InteropServices;
namespace LightningDB.Comparers;
@@ -21,10 +20,10 @@ public int Compare(MDBValue x, MDBValue y)
var right = y.AsSpan();
if (left.Length == 4 && right.Length == 4)
- return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right));
+ return x.Read().CompareTo(y.Read());
if (left.Length == 8 && right.Length == 8)
- return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right));
+ return x.Read().CompareTo(y.Read());
return left.SequenceCompareTo(right);
}
diff --git a/src/LightningDB/DatabaseConfiguration.cs b/src/LightningDB/DatabaseConfiguration.cs
index b8b66f5..ca7a3f5 100644
--- a/src/LightningDB/DatabaseConfiguration.cs
+++ b/src/LightningDB/DatabaseConfiguration.cs
@@ -37,18 +37,28 @@ internal IDisposable ConfigureDatabase(LightningTransaction tx, LightningDatabas
var pinnedComparer = new ComparerKeepAlive();
if (_comparer != null)
{
- CompareFunction compare = (ref left, ref right) => _comparer.Compare(left, right);
+ CompareFunction compare = Compare;
pinnedComparer.AddComparer(compare);
mdb_set_compare(tx._handle, db._handle, compare);
}
if (_duplicatesComparer == null) return pinnedComparer;
- CompareFunction dupCompare = (ref left, ref right) => _duplicatesComparer.Compare(left, right);
+ CompareFunction dupCompare = IsDuplicate;
pinnedComparer.AddComparer(dupCompare);
mdb_set_dupsort(tx._handle, db._handle, dupCompare);
return pinnedComparer;
}
+ private int Compare(ref MDBValue left, ref MDBValue right)
+ {
+ return _comparer.Compare(left, right);
+ }
+
+ private int IsDuplicate(ref MDBValue left, ref MDBValue right)
+ {
+ return _duplicatesComparer.Compare(left, right);
+ }
+
///
/// Sets a custom comparer for database operations using the specified comparer.
///
diff --git a/src/LightningDB/LightningCursor.cs b/src/LightningDB/LightningCursor.cs
index 0146b3a..20cd388 100644
--- a/src/LightningDB/LightningCursor.cs
+++ b/src/LightningDB/LightningCursor.cs
@@ -1,5 +1,5 @@
using System;
-using System.Linq;
+using System.Buffers;
using System.Runtime.CompilerServices;
using static LightningDB.Native.Lmdb;
@@ -377,10 +377,11 @@ public unsafe MDBResultCode Put(ReadOnlySpan key, ReadOnlySpan value
/// Returns
public unsafe MDBResultCode Put(byte[] key, byte[][] values)
{
- const int StackAllocateLimit = 256;//I just made up a number, this can be much more aggressive -arc
-
- var overallLength = values.Sum(arr => arr.Length);//probably allocates but boy is it handy...
+ const int StackAllocateLimit = 256;
+ var overallLength = 0;
+ for (var i = 0; i < values.Length; i++)
+ overallLength += values[i].Length;
//the idea here is to gain some perf by stackallocating the buffer to
//hold the contiguous keys
@@ -391,11 +392,16 @@ public unsafe MDBResultCode Put(byte[] key, byte[][] values)
return InnerPutMultiple(contiguousValues);
}
- fixed (byte* contiguousValuesPtr = new byte[overallLength])
+ var rentedArray = ArrayPool.Shared.Rent(overallLength);
+ try
{
- var contiguousValues = new Span(contiguousValuesPtr, overallLength);
+ var contiguousValues = rentedArray.AsSpan(0, overallLength);
return InnerPutMultiple(contiguousValues);
}
+ finally
+ {
+ ArrayPool.Shared.Return(rentedArray);
+ }
//these local methods could be made static, but the compiler will emit these closures
//as structs with very little overhead. Also static local functions isn't available
diff --git a/src/LightningDB/MDBValue.cs b/src/LightningDB/MDBValue.cs
index 0440eac..6c291a8 100644
--- a/src/LightningDB/MDBValue.cs
+++ b/src/LightningDB/MDBValue.cs
@@ -1,4 +1,6 @@
-using System;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace LightningDB;
@@ -6,22 +8,25 @@ namespace LightningDB;
/// A managed version of the native MDB_val type
///
///
-/// For Performance and Correctness, the layout of this struct must not be changed.
+/// For Performance and Correctness, the layout of this struct must not be changed.
/// This struct is blittable and is marshalled directly to Native code via
/// P/Invoke.
///
-public unsafe struct MDBValue
+#if NET5_0_OR_GREATER
+[SkipLocalsInit]
+#endif
+public readonly unsafe struct MDBValue
{
///
/// We only expose this shape constructor to basically force you to use
- /// a fixed statement to obtain the pointer. If we accepted a Span or
+ /// a fixed statement to obtain the pointer. If we accepted a Span or
/// ReadOnlySpan here, we would have to do scarier things to pin/unpin
/// the buffer. Since this library is geared towards safe and easy usage,
/// this way somewhat forces you onto the correct path.
- ///
+ ///
///
/// The length of the buffer
- /// A pointer to a buffer.
+ /// A pointer to a buffer.
/// The underlying memory may be managed(an array), unmanaged or stack-allocated.
/// If it is managed, it **MUST** be pinned via either GCHandle.Alloc or a fixed statement
///
@@ -31,20 +36,62 @@ internal MDBValue(int bufferSize, byte* pinnedOrStackAllocBuffer)
data = pinnedOrStackAllocBuffer;
}
//DO NOT REORDER
- internal nint size;
+ internal readonly nint size;
//DO NOT REORDER
- internal byte* data;
+ internal readonly byte* data;
///
- /// Gets a span representation of the buffer
+ /// Gets a read-only span representation of the buffer
///
- public ReadOnlySpan AsSpan() => new (data, (int)size);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+#if NETSTANDARD2_0
+ public ReadOnlySpan AsSpan() => new ReadOnlySpan(data, (int)size);
+#else
+ public ReadOnlySpan AsSpan() =>
+ MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(data), (int)size);
+#endif
+
+ ///
+ /// Gets a writable span representation of the buffer
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span AsWritableSpan() => new Span(data, (int)size);
+
+ ///
+ /// Reads a value of type T from the buffer
+ ///
+ /// The unmanaged type to read
+ /// The value read from the buffer
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T Read() where T : unmanaged =>
+ Unsafe.ReadUnaligned(data);
+
+ ///
+ /// Casts the buffer to a read-only span of type T
+ ///
+ /// The unmanaged type to cast to
+ /// A read-only span of the specified type
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ReadOnlySpan Cast() where T : unmanaged =>
+ MemoryMarshal.Cast(AsSpan());
+
+ ///
+ /// Copies the buffer contents to the destination span
+ ///
+ /// The destination span to copy to
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void CopyTo(Span destination) =>
+ Unsafe.CopyBlockUnaligned(
+ ref MemoryMarshal.GetReference(destination),
+ ref Unsafe.AsRef(data),
+ (uint)size);
///
/// Copies the data of the buffer to a new array
///
/// A newly allocated array containing data copied from the de-referenced data pointer
/// Equivalent to AsSpan().ToArray() but makes intent a little more clear
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] CopyToNewArray() => AsSpan().ToArray();
-}
\ No newline at end of file
+}