Skip to content

Commit 80740fc

Browse files
committed
Adding Guid comparer option
1 parent c30be38 commit 80740fc

5 files changed

Lines changed: 374 additions & 0 deletions

File tree

src/LightningDB.Benchmarks/ComparerDescriptor.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ private ComparerDescriptor(string name, IComparer<MDBValue> comparer)
3737
new ComparerDescriptor("ReverseLength", ReverseLengthComparer.Instance),
3838
new ComparerDescriptor("LengthOnly", LengthOnlyComparer.Instance),
3939
new ComparerDescriptor("HashCode", HashCodeComparer.Instance),
40+
new ComparerDescriptor("Guid", GuidComparer.Instance),
41+
new ComparerDescriptor("ReverseGuid", ReverseGuidComparer.Instance),
4042
};
4143

4244
/// <summary>
@@ -50,4 +52,15 @@ private ComparerDescriptor(string name, IComparer<MDBValue> comparer)
5052
new ComparerDescriptor("UnsignedInt", UnsignedIntegerComparer.Instance),
5153
new ComparerDescriptor("ReverseUnsignedInt", ReverseUnsignedIntegerComparer.Instance),
5254
};
55+
56+
/// <summary>
57+
/// GUID comparers only (for focused 16-byte key benchmarks).
58+
/// </summary>
59+
public static IEnumerable<ComparerDescriptor> GuidComparers => new[]
60+
{
61+
new ComparerDescriptor("Default", null),
62+
new ComparerDescriptor("Bitwise", BitwiseComparer.Instance),
63+
new ComparerDescriptor("Guid", GuidComparer.Instance),
64+
new ComparerDescriptor("ReverseGuid", ReverseGuidComparer.Instance),
65+
};
5366
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using BenchmarkDotNet.Attributes;
5+
6+
namespace LightningDB.Benchmarks;
7+
8+
/// <summary>
9+
/// Focused benchmarks for GUID comparers testing their optimized 16-byte path.
10+
/// GuidComparer uses two ulong comparisons with big-endian reads for byte ordering.
11+
/// </summary>
12+
[MemoryDiagnoser]
13+
public class GuidComparerBenchmarks
14+
{
15+
private string _path;
16+
private LightningEnvironment _env;
17+
private LightningDatabase _db;
18+
private byte[][] _keys;
19+
private byte[] _valueBuffer;
20+
21+
[ParamsSource(nameof(GuidComparers))]
22+
public ComparerDescriptor Comparer { get; set; }
23+
24+
public static IEnumerable<ComparerDescriptor> GuidComparers
25+
=> ComparerDescriptor.GuidComparers;
26+
27+
[Params(1000, 10000)]
28+
public int OpsPerTransaction { get; set; }
29+
30+
[GlobalSetup]
31+
public void GlobalSetup()
32+
{
33+
Console.WriteLine($"Global Setup Begin - Comparer: {Comparer.Name}");
34+
35+
_path = $"GuidBenchDir_{Guid.NewGuid():N}";
36+
if (Directory.Exists(_path))
37+
Directory.Delete(_path, true);
38+
39+
_env = new LightningEnvironment(_path) { MaxDatabases = 1 };
40+
_env.Open();
41+
42+
var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
43+
if (Comparer.Comparer != null)
44+
config.CompareWith(Comparer.Comparer);
45+
46+
using (var tx = _env.BeginTransaction()) {
47+
_db = tx.OpenDatabase(configuration: config);
48+
tx.Commit();
49+
}
50+
51+
_valueBuffer = new byte[64];
52+
_keys = GenerateGuidKeys(OpsPerTransaction);
53+
54+
Console.WriteLine("Global Setup End");
55+
}
56+
57+
private static byte[][] GenerateGuidKeys(int count)
58+
{
59+
var keys = new byte[count][];
60+
61+
for (var i = 0; i < count; i++) {
62+
keys[i] = Guid.NewGuid().ToByteArray();
63+
}
64+
65+
return keys;
66+
}
67+
68+
[Benchmark]
69+
public void WriteGuids()
70+
{
71+
using var tx = _env.BeginTransaction();
72+
73+
for (var i = 0; i < OpsPerTransaction; i++) {
74+
tx.Put(_db, _keys[i], _valueBuffer);
75+
}
76+
77+
tx.Commit();
78+
}
79+
80+
[GlobalCleanup]
81+
public void GlobalCleanup()
82+
{
83+
Console.WriteLine("Global Cleanup Begin");
84+
85+
try {
86+
_db?.Dispose();
87+
_env?.Dispose();
88+
89+
if (Directory.Exists(_path))
90+
Directory.Delete(_path, true);
91+
}
92+
catch (Exception ex) {
93+
Console.WriteLine(ex.ToString());
94+
}
95+
96+
Console.WriteLine("Global Cleanup End");
97+
}
98+
}

src/LightningDB.Tests/ComparerTests.cs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,4 +461,171 @@ public void reverse_bitwise_comparer_works_with_duplicate_values()
461461

462462
cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.NotFound);
463463
}
464+
465+
public void guid_comparer_sorts_guids_by_byte_order()
466+
{
467+
using var env = CreateEnvironment();
468+
env.Open();
469+
470+
var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
471+
config.CompareWith(GuidComparer.Instance);
472+
473+
using var txn = env.BeginTransaction();
474+
using var db = txn.OpenDatabase(configuration: config);
475+
476+
// Create GUIDs with known byte patterns for predictable ordering
477+
// First byte differs: 0x01 < 0x02 < 0xFF
478+
var guid1 = new Guid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
479+
var guid2 = new Guid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
480+
var guid3 = new Guid(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
481+
482+
txn.Put(db, guid2.ToByteArray(), new byte[] { 2 });
483+
txn.Put(db, guid3.ToByteArray(), new byte[] { 3 });
484+
txn.Put(db, guid1.ToByteArray(), new byte[] { 1 });
485+
486+
using var cursor = txn.CreateCursor(db);
487+
488+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
489+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid1);
490+
491+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
492+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid2);
493+
494+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
495+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid3);
496+
497+
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
498+
}
499+
500+
public void guid_comparer_sorts_by_second_half_when_first_half_equal()
501+
{
502+
using var env = CreateEnvironment();
503+
env.Open();
504+
505+
var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
506+
config.CompareWith(GuidComparer.Instance);
507+
508+
using var txn = env.BeginTransaction();
509+
using var db = txn.OpenDatabase(configuration: config);
510+
511+
// Same first 8 bytes, differ in second half (byte 8)
512+
var guid1 = new Guid(new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
513+
var guid2 = new Guid(new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
514+
var guid3 = new Guid(new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
515+
516+
txn.Put(db, guid3.ToByteArray(), new byte[] { 3 });
517+
txn.Put(db, guid1.ToByteArray(), new byte[] { 1 });
518+
txn.Put(db, guid2.ToByteArray(), new byte[] { 2 });
519+
520+
using var cursor = txn.CreateCursor(db);
521+
522+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
523+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid1);
524+
525+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
526+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid2);
527+
528+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
529+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid3);
530+
531+
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
532+
}
533+
534+
public void reverse_guid_comparer_sorts_guids_descending()
535+
{
536+
using var env = CreateEnvironment();
537+
env.Open();
538+
539+
var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
540+
config.CompareWith(ReverseGuidComparer.Instance);
541+
542+
using var txn = env.BeginTransaction();
543+
using var db = txn.OpenDatabase(configuration: config);
544+
545+
var guid1 = new Guid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
546+
var guid2 = new Guid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
547+
var guid3 = new Guid(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
548+
549+
txn.Put(db, guid1.ToByteArray(), new byte[] { 1 });
550+
txn.Put(db, guid2.ToByteArray(), new byte[] { 2 });
551+
txn.Put(db, guid3.ToByteArray(), new byte[] { 3 });
552+
553+
using var cursor = txn.CreateCursor(db);
554+
555+
// Reverse order: FF, 02, 01
556+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
557+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid3);
558+
559+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
560+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid2);
561+
562+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
563+
new Guid(cursor.GetCurrent().key.CopyToNewArray()).ShouldBe(guid1);
564+
565+
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
566+
}
567+
568+
public void guid_comparer_falls_back_for_non_16_byte_values()
569+
{
570+
using var env = CreateEnvironment();
571+
env.Open();
572+
573+
var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create };
574+
config.CompareWith(GuidComparer.Instance);
575+
576+
using var txn = env.BeginTransaction();
577+
using var db = txn.OpenDatabase(configuration: config);
578+
579+
// Non-16-byte keys should still work via fallback to SequenceCompareTo
580+
txn.Put(db, new byte[] { 0xFF, 0xFF }, new byte[] { 1 });
581+
txn.Put(db, new byte[] { 0x00, 0x00 }, new byte[] { 2 });
582+
txn.Put(db, new byte[] { 0x80, 0x80 }, new byte[] { 3 });
583+
584+
using var cursor = txn.CreateCursor(db);
585+
586+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
587+
cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0x00, 0x00 });
588+
589+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
590+
cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0x80, 0x80 });
591+
592+
cursor.Next().Item1.ShouldBe(MDBResultCode.Success);
593+
cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0xFF, 0xFF });
594+
595+
cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound);
596+
}
597+
598+
public void guid_comparer_works_with_duplicate_values()
599+
{
600+
using var env = CreateEnvironment();
601+
env.Open();
602+
603+
var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create | DatabaseOpenFlags.DuplicatesSort };
604+
config.FindDuplicatesWith(GuidComparer.Instance);
605+
606+
using var txn = env.BeginTransaction();
607+
using var db = txn.OpenDatabase(configuration: config);
608+
609+
var key = new byte[] { 1 };
610+
var val1 = new Guid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
611+
var val2 = new Guid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
612+
var val3 = new Guid(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
613+
614+
txn.Put(db, key, val3.ToByteArray());
615+
txn.Put(db, key, val1.ToByteArray());
616+
txn.Put(db, key, val2.ToByteArray());
617+
618+
using var cursor = txn.CreateCursor(db);
619+
cursor.SetKey(key);
620+
621+
new Guid(cursor.GetCurrent().value.CopyToNewArray()).ShouldBe(val1);
622+
623+
cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.Success);
624+
new Guid(cursor.GetCurrent().value.CopyToNewArray()).ShouldBe(val2);
625+
626+
cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.Success);
627+
new Guid(cursor.GetCurrent().value.CopyToNewArray()).ShouldBe(val3);
628+
629+
cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.NotFound);
630+
}
464631
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Buffers.Binary;
3+
using System.Collections.Generic;
4+
using System.Runtime.CompilerServices;
5+
6+
namespace LightningDB.Comparers;
7+
8+
/// <summary>
9+
/// Compares MDBValue instances as GUIDs using byte ordering (lexicographic).
10+
/// Optimized for 16-byte values using two ulong comparisons with early termination.
11+
/// Falls back to bitwise comparison for non-16-byte inputs.
12+
/// </summary>
13+
/// <remarks>
14+
/// This comparer uses byte-level ordering (memcmp-style), NOT Guid.CompareTo() ordering.
15+
/// GUIDs are compared as raw byte sequences from first byte to last.
16+
/// </remarks>
17+
public sealed class GuidComparer : IComparer<MDBValue>
18+
{
19+
public static readonly GuidComparer Instance = new();
20+
21+
private GuidComparer() { }
22+
23+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
24+
public int Compare(MDBValue x, MDBValue y)
25+
{
26+
var left = x.AsSpan();
27+
var right = y.AsSpan();
28+
29+
if (left.Length == 16 && right.Length == 16)
30+
{
31+
// Compare first 8 bytes (big-endian for correct byte ordering)
32+
var leftHigh = BinaryPrimitives.ReadUInt64BigEndian(left);
33+
var rightHigh = BinaryPrimitives.ReadUInt64BigEndian(right);
34+
35+
var cmp = leftHigh.CompareTo(rightHigh);
36+
if (cmp != 0)
37+
return cmp;
38+
39+
// Compare last 8 bytes
40+
var leftLow = BinaryPrimitives.ReadUInt64BigEndian(left.Slice(8));
41+
var rightLow = BinaryPrimitives.ReadUInt64BigEndian(right.Slice(8));
42+
43+
return leftLow.CompareTo(rightLow);
44+
}
45+
46+
return left.SequenceCompareTo(right);
47+
}
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Buffers.Binary;
3+
using System.Collections.Generic;
4+
using System.Runtime.CompilerServices;
5+
6+
namespace LightningDB.Comparers;
7+
8+
/// <summary>
9+
/// Compares MDBValue instances as GUIDs in reverse byte order (descending).
10+
/// Optimized for 16-byte values using two ulong comparisons with early termination.
11+
/// Falls back to reverse bitwise comparison for non-16-byte inputs.
12+
/// </summary>
13+
/// <remarks>
14+
/// This comparer uses reverse byte-level ordering, NOT reverse Guid.CompareTo() ordering.
15+
/// GUIDs are compared as raw byte sequences from first byte to last, then reversed.
16+
/// </remarks>
17+
public sealed class ReverseGuidComparer : IComparer<MDBValue>
18+
{
19+
public static readonly ReverseGuidComparer Instance = new();
20+
21+
private ReverseGuidComparer() { }
22+
23+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
24+
public int Compare(MDBValue x, MDBValue y)
25+
{
26+
var left = x.AsSpan();
27+
var right = y.AsSpan();
28+
29+
if (left.Length == 16 && right.Length == 16)
30+
{
31+
// Compare first 8 bytes (reversed: right vs left)
32+
var leftHigh = BinaryPrimitives.ReadUInt64BigEndian(left);
33+
var rightHigh = BinaryPrimitives.ReadUInt64BigEndian(right);
34+
35+
var cmp = rightHigh.CompareTo(leftHigh);
36+
if (cmp != 0)
37+
return cmp;
38+
39+
// Compare last 8 bytes (reversed)
40+
var leftLow = BinaryPrimitives.ReadUInt64BigEndian(left.Slice(8));
41+
var rightLow = BinaryPrimitives.ReadUInt64BigEndian(right.Slice(8));
42+
43+
return rightLow.CompareTo(leftLow);
44+
}
45+
46+
return right.SequenceCompareTo(left);
47+
}
48+
}

0 commit comments

Comments
 (0)