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 +}