From 6eb1ba34ad805c27265ce53474b9023ffcf67ebe Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 07:12:37 -0800 Subject: [PATCH 01/13] Allow optionally passed string allocator and expand benchmarks --- MaxMind.Db.Benchmark/Program.cs | 103 ++++++++++++++++++++++++++++++- MaxMind.Db/AllocatorDelegates.cs | 16 +++++ MaxMind.Db/ArrayBuffer.cs | 10 +++ MaxMind.Db/Buffer.cs | 18 ++++++ MaxMind.Db/Decoder.cs | 3 +- MaxMind.Db/MaxMind.Db.csproj | 3 + MaxMind.Db/MemoryMapBuffer.cs | 14 +++++ MaxMind.Db/Reader.cs | 12 ++-- 8 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 MaxMind.Db/AllocatorDelegates.cs diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index 2696b0a7..d3c51463 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Net; +using System.Text; BenchmarkRunner.Run(new DebugInProcessConfig()); @@ -15,6 +17,9 @@ public class CityBenchmark { // A random IP that has city info. private Reader _reader = null!; + private Reader _stringInternedReader = null!; + private Reader _ArrayBufferReader = null!; + private IPAddress[] _ipAddresses = []; [GlobalSetup] @@ -24,6 +29,8 @@ public void GlobalSetup() string dbPath = Environment.GetEnvironmentVariable(dbPathVarName) ?? throw new InvalidOperationException($"{dbPathVarName} was not set"); _reader = new Reader(dbPath); + _ArrayBufferReader = new Reader(dbPath, FileAccessMode.Memory); + _stringInternedReader = new Reader(dbPath, FileAccessMode.Memory); const string ipAddressesVarName = "MAXMIND_BENCHMARK_IP_ADDRESSES"; string ipAddressesStr = Environment.GetEnvironmentVariable(ipAddressesVarName) ?? ""; @@ -51,7 +58,7 @@ public void GlobalCleanup() } [Benchmark] - public int City() + public int CityMemoryMappedLookup() { int x = 0; foreach (var ipAddress in _ipAddresses) @@ -64,6 +71,36 @@ public int City() return x; } + + [Benchmark] + public int CityMemoryLookup() + { + int x = 0; + foreach (var ipAddress in _ipAddresses) + { + if (_ArrayBufferReader.Find(ipAddress) != null) + { + x += 1; + } + } + + return x; + } + + [Benchmark] + public int CityInternedStringsLookup() + { + int x = 0; + foreach (var ipAddress in _ipAddresses) + { + if (_stringInternedReader.Find(ipAddress) != null) + { + x += 1; + } + } + + return x; + } } public abstract class AbstractCountryResponse @@ -236,4 +273,66 @@ public Subdivision( public int? Confidence { get; internal set; } public string? IsoCode { get; internal set; } -} \ No newline at end of file +} + +public sealed class ReadOnlyMemoryByteComparer : IEqualityComparer> + +{ + public static ReadOnlyMemoryByteComparer Default { get; } = new ReadOnlyMemoryByteComparer(); + + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + { + // Use the sequence equal method to compare the contents + return x.Span.SequenceEqual(y.Span); + } + + public int GetHashCode(ReadOnlyMemory obj) + { + // This is a simple, non-cryptographic hash code generation based on the content. + // For production use, consider a more robust hashing algorithm for byte sequences. + // The implementation below sums up bytes in chunks of int32 for performance. + // This is a basic approach and might not be suitable for high-security scenarios + // due to potential hash collisions (similar to string hashing behavior in dictionaries). + + unchecked + { + int hash = 17; + ReadOnlySpan span = obj.Span; + + // Simple hashing of the sequence + foreach (byte b in span) + { + hash = hash * 31 + b; + } + return hash; + } + } +} + + +/// +/// +/// +public static class InternedStrings +{ + internal static Dictionary, string> s_Dictionary = new (ReadOnlyMemoryByteComparer.Default); + + /// + /// + /// + /// + /// + public static string GetString(ReadOnlyMemory bytes) + { + bool found = s_Dictionary.TryGetValue(bytes, out string? returnValue); + + if (!found) + { + returnValue = Encoding.UTF8.GetString(bytes.Span); + s_Dictionary.TryAdd(bytes, returnValue); + } + + Debug.Assert(returnValue is not null); + return returnValue; + } +} diff --git a/MaxMind.Db/AllocatorDelegates.cs b/MaxMind.Db/AllocatorDelegates.cs new file mode 100644 index 00000000..7b1c0272 --- /dev/null +++ b/MaxMind.Db/AllocatorDelegates.cs @@ -0,0 +1,16 @@ +using System; + +namespace MaxMind.Db; + +/// +/// Delegate methods for allocations +/// +public static class AllocatorDelegates +{ + /// + /// Allocate a string based on a sequence of bytes + /// + /// + /// + public delegate string GetString(ReadOnlyMemory bytes); +} \ No newline at end of file diff --git a/MaxMind.Db/ArrayBuffer.cs b/MaxMind.Db/ArrayBuffer.cs index 36bfa00d..2381ed67 100644 --- a/MaxMind.Db/ArrayBuffer.cs +++ b/MaxMind.Db/ArrayBuffer.cs @@ -17,6 +17,16 @@ public ArrayBuffer(byte[] array) _fileBytes = array; } + public override ReadOnlySpan AsSpan(long offset, int count) + { + return _fileBytes.AsSpan().Slice((int)offset, count); + } + + public override ReadOnlyMemory AsMemory(long offset, int count) + { + return _fileBytes.AsMemory().Slice((int)offset, count); + } + public override byte[] Read(long offset, int count) { var bytes = new byte[count]; diff --git a/MaxMind.Db/Buffer.cs b/MaxMind.Db/Buffer.cs index cf609f97..02d8cf90 100644 --- a/MaxMind.Db/Buffer.cs +++ b/MaxMind.Db/Buffer.cs @@ -21,6 +21,24 @@ internal abstract class Buffer : IDisposable public long Length { get; protected set; } + /// + /// Return a slice of the buffer as a read only span. Prefer over the + /// Read method + /// + /// + /// + /// + public abstract ReadOnlySpan AsSpan(long offset, int size); + + /// + /// Return a slice of the buffer as a read only memory. Prefer over the + /// Read method + /// + /// + /// + /// + public abstract ReadOnlyMemory AsMemory(long offset, int size); + /// /// Read a big integer from the buffer. /// diff --git a/MaxMind.Db/Decoder.cs b/MaxMind.Db/Decoder.cs index a8103e9b..dd26e028 100644 --- a/MaxMind.Db/Decoder.cs +++ b/MaxMind.Db/Decoder.cs @@ -54,7 +54,8 @@ internal sealed class Decoder /// The database. /// The base address in the stream. /// Whether to follow pointers. For testing. - internal Decoder(Buffer database, long pointerBase, bool followPointers = true) + /// Optional method to allocate strings + internal Decoder(Buffer database, long pointerBase, bool followPointers = true, AllocatorDelegates.GetString? stringAllocator = null) { _pointerBase = pointerBase; _database = database; diff --git a/MaxMind.Db/MaxMind.Db.csproj b/MaxMind.Db/MaxMind.Db.csproj index 21201247..a05167bd 100644 --- a/MaxMind.Db/MaxMind.Db.csproj +++ b/MaxMind.Db/MaxMind.Db.csproj @@ -50,4 +50,7 @@ + + + diff --git a/MaxMind.Db/MemoryMapBuffer.cs b/MaxMind.Db/MemoryMapBuffer.cs index 10d24648..0761d3b3 100644 --- a/MaxMind.Db/MemoryMapBuffer.cs +++ b/MaxMind.Db/MemoryMapBuffer.cs @@ -245,6 +245,20 @@ private static (MemoryMappedFile File, MemoryMappedViewAccessor View) CreateMmap } } + public override ReadOnlySpan AsSpan(long offset, int count) + { + var bytes = new byte[count]; + _view.ReadArray(offset, bytes, 0, bytes.Length); + return bytes.AsSpan(); + } + + public override ReadOnlyMemory AsMemory(long offset, int count) + { + var bytes = new byte[count]; + _view.ReadArray(offset, bytes, 0, bytes.Length); + return bytes.AsMemory(); + } + public override byte[] Read(long offset, int count) { var bytes = new byte[count]; diff --git a/MaxMind.Db/Reader.cs b/MaxMind.Db/Reader.cs index 3d43918f..3aec7d84 100644 --- a/MaxMind.Db/Reader.cs +++ b/MaxMind.Db/Reader.cs @@ -106,7 +106,8 @@ private struct NetNode /// Initializes a new instance of the class. /// /// The file. - public Reader(string file) : this(file, FileAccessMode.MemoryMapped) + public Reader(string file) + : this(file, FileAccessMode.MemoryMapped) { } @@ -115,7 +116,8 @@ public Reader(string file) : this(file, FileAccessMode.MemoryMapped) /// /// The MaxMind DB file. /// The mode by which to access the DB file. - public Reader(string file, FileAccessMode mode) : this(BufferForMode(file, mode), file) + public Reader(string file, FileAccessMode mode) + : this(BufferForMode(file, mode), file) { } @@ -161,7 +163,8 @@ private Reader(Buffer buffer, string? file) /// Asynchronously initializes a new instance of the class by reading the specified file into a memory-mapped region. /// /// The file. - public static async Task CreateAsync(string file) + /// Optional allocator method for strings created from byte arrays. + public static async Task CreateAsync(string file, AllocatorDelegates.GetString? stringAllocator = null) { return new Reader(await MemoryMapBuffer.CreateAsync(file).ConfigureAwait(false), file); } @@ -170,8 +173,9 @@ public static async Task CreateAsync(string file) /// Asynchronously initialize with Stream. /// /// The stream to use. It will be used from its current position. + /// Optional allocator method for strings created from byte arrays. /// - public static async Task CreateAsync(Stream stream) + public static async Task CreateAsync(Stream stream, AllocatorDelegates.GetString? stringAllocator = null) { return new Reader(await MemoryMapBuffer.CreateAsync(stream).ConfigureAwait(false), null); } From 912eab9c414a40ed615d5d77011a7a49d05c10c2 Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 07:14:17 -0800 Subject: [PATCH 02/13] Remove comments --- MaxMind.Db.Benchmark/Program.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index d3c51463..af23fc78 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -282,18 +282,11 @@ public sealed class ReadOnlyMemoryByteComparer : IEqualityComparer x, ReadOnlyMemory y) { - // Use the sequence equal method to compare the contents return x.Span.SequenceEqual(y.Span); } public int GetHashCode(ReadOnlyMemory obj) { - // This is a simple, non-cryptographic hash code generation based on the content. - // For production use, consider a more robust hashing algorithm for byte sequences. - // The implementation below sums up bytes in chunks of int32 for performance. - // This is a basic approach and might not be suitable for high-security scenarios - // due to potential hash collisions (similar to string hashing behavior in dictionaries). - unchecked { int hash = 17; From 521c907c6539020012a30da26de7208f088d6ec0 Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 07:14:47 -0800 Subject: [PATCH 03/13] More small cleanup --- MaxMind.Db.Benchmark/Program.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index af23fc78..9e63e0ec 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -302,19 +302,10 @@ public int GetHashCode(ReadOnlyMemory obj) } } - -/// -/// -/// public static class InternedStrings { internal static Dictionary, string> s_Dictionary = new (ReadOnlyMemoryByteComparer.Default); - - /// - /// - /// - /// - /// + public static string GetString(ReadOnlyMemory bytes) { bool found = s_Dictionary.TryGetValue(bytes, out string? returnValue); From f91c397c2195284b52f844381d97809ba5591655 Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 07:44:14 -0800 Subject: [PATCH 04/13] Fix build error on windows --- MaxMind.Db.Benchmark/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index 9e63e0ec..b53f1476 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -312,7 +312,11 @@ public static string GetString(ReadOnlyMemory bytes) if (!found) { +#if NETCOREAPP2_1_OR_GREATER returnValue = Encoding.UTF8.GetString(bytes.Span); +#else + returnValue = Encoding.UTF8.GetString(bytes.Span.ToArray()); +#endif s_Dictionary.TryAdd(bytes, returnValue); } From 92adc2c0c4bd43dfda2eac76e30720fe17303d85 Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 07:50:48 -0800 Subject: [PATCH 05/13] Fix build part 2. --- MaxMind.Db.Benchmark/Program.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index b53f1476..1f1248a6 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -87,6 +87,7 @@ public int CityMemoryLookup() return x; } +#if NETCOREAPP2_1_OR_GREATER [Benchmark] public int CityInternedStringsLookup() { @@ -101,6 +102,8 @@ public int CityInternedStringsLookup() return x; } +#endif + } public abstract class AbstractCountryResponse @@ -275,6 +278,8 @@ public Subdivision( public string? IsoCode { get; internal set; } } +#if NETCOREAPP2_1_OR_GREATER + public sealed class ReadOnlyMemoryByteComparer : IEqualityComparer> { @@ -312,15 +317,13 @@ public static string GetString(ReadOnlyMemory bytes) if (!found) { -#if NETCOREAPP2_1_OR_GREATER returnValue = Encoding.UTF8.GetString(bytes.Span); -#else - returnValue = Encoding.UTF8.GetString(bytes.Span.ToArray()); -#endif s_Dictionary.TryAdd(bytes, returnValue); } Debug.Assert(returnValue is not null); - return returnValue; + return returnValue ?? new string(); } } + +#endif \ No newline at end of file From 7109e4b69f75159f09cf3a9e5a46d6599503b2c1 Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 07:53:42 -0800 Subject: [PATCH 06/13] More broken builds.... --- MaxMind.Db.Benchmark/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index 1f1248a6..11ec467c 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -17,7 +17,7 @@ public class CityBenchmark { // A random IP that has city info. private Reader _reader = null!; - private Reader _stringInternedReader = null!; + private Reader? _stringInternedReader = null!; private Reader _ArrayBufferReader = null!; private IPAddress[] _ipAddresses = []; @@ -94,7 +94,7 @@ public int CityInternedStringsLookup() int x = 0; foreach (var ipAddress in _ipAddresses) { - if (_stringInternedReader.Find(ipAddress) != null) + if (_stringInternedReader?.Find(ipAddress) != null) { x += 1; } @@ -322,7 +322,7 @@ public static string GetString(ReadOnlyMemory bytes) } Debug.Assert(returnValue is not null); - return returnValue ?? new string(); + return returnValue; } } From eaf4ecdbdac44d354c6bd427767435010f71578f Mon Sep 17 00:00:00 2001 From: jashook Date: Wed, 25 Feb 2026 09:19:48 -0800 Subject: [PATCH 07/13] Painful windows build --- MaxMind.Db.Benchmark/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index 11ec467c..74865a0c 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -17,7 +17,11 @@ public class CityBenchmark { // A random IP that has city info. private Reader _reader = null!; + +#if NETCOREAPP2_1_OR_GREATER private Reader? _stringInternedReader = null!; +#endif + private Reader _ArrayBufferReader = null!; private IPAddress[] _ipAddresses = []; From decb60cdd5fdbd703a87dd48ab1ae975b8545e99 Mon Sep 17 00:00:00 2001 From: jashook Date: Sun, 1 Mar 2026 20:52:45 -0800 Subject: [PATCH 08/13] Take pr feedback --- MaxMind.Db.Benchmark/Program.cs | 44 +++++++----- MaxMind.Db/Decoder.cs | 114 ++++++++++++++++++++++++++++++-- MaxMind.Db/Reader.cs | 21 ++++-- MaxMind.Db/SNEGenericCache.cs | 94 ++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 28 deletions(-) create mode 100644 MaxMind.Db/SNEGenericCache.cs diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index 74865a0c..ca8e03e4 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -16,13 +16,11 @@ public class CityBenchmark { // A random IP that has city info. - private Reader _reader = null!; + private Reader _memMapReader = null!; -#if NETCOREAPP2_1_OR_GREATER - private Reader? _stringInternedReader = null!; -#endif - - private Reader _ArrayBufferReader = null!; + private Reader _arrayBufferCachedReader = null!; + private Reader _memMapCachedReader = null!; + private Reader _arrayBufferReader = null!; private IPAddress[] _ipAddresses = []; @@ -32,9 +30,10 @@ public void GlobalSetup() const string dbPathVarName = "MAXMIND_BENCHMARK_DB"; string dbPath = Environment.GetEnvironmentVariable(dbPathVarName) ?? throw new InvalidOperationException($"{dbPathVarName} was not set"); - _reader = new Reader(dbPath); - _ArrayBufferReader = new Reader(dbPath, FileAccessMode.Memory); - _stringInternedReader = new Reader(dbPath, FileAccessMode.Memory); + _memMapReader = new Reader(dbPath, FileAccessMode.MemoryMapped); + _memMapCachedReader = new Reader(dbPath, FileAccessMode.MemoryMapped, 4_096); + _arrayBufferReader = new Reader(dbPath, FileAccessMode.Memory); + _arrayBufferCachedReader = new Reader(dbPath, FileAccessMode.Memory, 4_096); const string ipAddressesVarName = "MAXMIND_BENCHMARK_IP_ADDRESSES"; string ipAddressesStr = Environment.GetEnvironmentVariable(ipAddressesVarName) ?? ""; @@ -58,7 +57,7 @@ public void GlobalSetup() [GlobalCleanup] public void GlobalCleanup() { - _reader.Dispose(); + _memMapReader.Dispose(); } [Benchmark] @@ -67,7 +66,22 @@ public int CityMemoryMappedLookup() int x = 0; foreach (var ipAddress in _ipAddresses) { - if (_reader.Find(ipAddress) != null) + if (_memMapReader.Find(ipAddress) != null) + { + x += 1; + } + } + + return x; + } + + [Benchmark] + public int CityMemoryMappedCachedLookup() + { + int x = 0; + foreach (var ipAddress in _ipAddresses) + { + if (_memMapCachedReader.Find(ipAddress) != null) { x += 1; } @@ -82,7 +96,7 @@ public int CityMemoryLookup() int x = 0; foreach (var ipAddress in _ipAddresses) { - if (_ArrayBufferReader.Find(ipAddress) != null) + if (_arrayBufferReader.Find(ipAddress) != null) { x += 1; } @@ -91,14 +105,13 @@ public int CityMemoryLookup() return x; } -#if NETCOREAPP2_1_OR_GREATER [Benchmark] - public int CityInternedStringsLookup() + public int CityMemoryCachedLookup() { int x = 0; foreach (var ipAddress in _ipAddresses) { - if (_stringInternedReader?.Find(ipAddress) != null) + if (_arrayBufferCachedReader?.Find(ipAddress) != null) { x += 1; } @@ -106,7 +119,6 @@ public int CityInternedStringsLookup() return x; } -#endif } diff --git a/MaxMind.Db/Decoder.cs b/MaxMind.Db/Decoder.cs index dd26e028..37871df3 100644 --- a/MaxMind.Db/Decoder.cs +++ b/MaxMind.Db/Decoder.cs @@ -6,6 +6,7 @@ #endif using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Numerics; #endregion @@ -47,6 +48,7 @@ internal sealed class Decoder private readonly DictionaryActivatorCreator _dictionaryActivatorCreator; private readonly ListActivatorCreator _listActivatorCreator; + private readonly SNEGenericCache? _cache; /// /// Initializes a new instance of the class. @@ -54,8 +56,8 @@ internal sealed class Decoder /// The database. /// The base address in the stream. /// Whether to follow pointers. For testing. - /// Optional method to allocate strings - internal Decoder(Buffer database, long pointerBase, bool followPointers = true, AllocatorDelegates.GetString? stringAllocator = null) + /// Whether to use a cache or not. + internal Decoder(Buffer database, long pointerBase, bool followPointers = true, int? defaultCacheSize = null) { _pointerBase = pointerBase; _database = database; @@ -63,6 +65,7 @@ internal Decoder(Buffer database, long pointerBase, bool followPointers = true, _listActivatorCreator = new ListActivatorCreator(); _dictionaryActivatorCreator = new DictionaryActivatorCreator(); _typeActivatorCreator = new TypeActivatorCreator(); + _cache = defaultCacheSize is null ? null : new (defaultCacheSize.Value); } /// @@ -85,7 +88,7 @@ internal T Decode(long offset, out long outOffset, InjectableValues? injectab private object Decode(Type expectedType, long offset, out long outOffset, InjectableValues? injectables = null, Network? network = null) { var type = CtrlData(offset, out var size, out offset); - return DecodeByType(expectedType, type, offset, size, out outOffset, injectables, network); + return DecodeByTypeFromCacheOrCreate(expectedType, type, offset, size, out outOffset, injectables, network); } private ObjectType CtrlData(long offset, out int size, out long outOffset) @@ -146,6 +149,38 @@ private ObjectType CtrlData(long offset, out int size, out long outOffset) /// /// /// Unable to handle type! + private object DecodeByTypeFromCacheOrCreate( + Type expectedType, + ObjectType type, + long offset, + int size, + out long outOffset, + InjectableValues? injectables, + Network? network + ) + { + ValueTuple returnValue = DecodeFromCacheOrCreate(offset, size, expectedType, type, injectables, network, static (Buffer database, long offset, int size, Type type, ObjectType objectType, Decoder decoder, InjectableValues? injectableValues, Network? network) => + { + long returnOffset = 0; + return (decoder.DecodeByType(type, objectType, offset, size, out returnOffset, injectableValues, network), returnOffset); + }); + + outOffset = returnValue.Item2; + return returnValue.Item1; + } + + /// + /// Decodes the type of the by. + /// + /// + /// The type. + /// The offset. + /// The size. + /// The out offset + /// + /// + /// + /// Unable to handle type! private object DecodeByType( Type expectedType, ObjectType type, @@ -274,14 +309,20 @@ private string DecodeString(Type expectedType, long offset, int size) { ReflectionUtil.CheckType(expectedType, typeof(string)); - return _database.ReadString(offset, size); + return (string)DecodeFromCacheOrCreate(offset, size, expectedType, static (Buffer database, long offset, int size) => + { + return (database.ReadString(offset, size), offset + size); + }).Item1; } private byte[] DecodeBytes(Type expectedType, long offset, int size) { ReflectionUtil.CheckType(expectedType, typeof(byte[])); - return _database.Read(offset, size); + return (byte[])DecodeFromCacheOrCreate(offset, size, expectedType, static (Buffer database, long offset, int size) => + { + return (database.ReadString(offset, size), offset + size); + }).Item1; } /// @@ -600,7 +641,13 @@ private ulong DecodeUInt64(Type expectedType, long offset, int size) private BigInteger DecodeBigInteger(Type expectedType, long offset, int size) { ReflectionUtil.CheckType(expectedType, typeof(BigInteger)); - return _database.ReadBigInteger(offset, size); + + // Note: this will box; however, the box is cheaper than the byte + // array allocation under the hood. + return (BigInteger)DecodeFromCacheOrCreate(offset, size, expectedType, static (Buffer database, long offset, int size) => + { + return (database.ReadBigInteger(offset, size), offset + size); + }).Item1; } /// @@ -631,5 +678,60 @@ private int DecodeInteger(Type expectedType, long offset, int size) return _database.ReadVarInt(offset, size); } + + private (object, long) DecodeFromCacheOrCreate(long offset, int size, Type type, Func factory) + { + if (_cache is not null) + { + ValueTuple item; + bool found = _cache.TryGet(offset, size, type, out item); + if (found) + { + // Not null if found. + Debug.Assert(item.Item1 is not null); + return item; + } + else + { + item = factory(_database, offset, size); + bool added = _cache.TryAdd(offset, size, type, item); + } + + return item; + } + + return factory(_database, offset, size); + } + + private (object, long) DecodeFromCacheOrCreate( + long offset, + int size, + Type type, + ObjectType objectType, + InjectableValues? injectableValues, + Network? network, + Func factory) + { + if (_cache is not null) + { + ValueTuple item; + bool found = _cache.TryGet(offset, size, type, out item); + if (found) + { + // Not null if found. + Debug.Assert(item.Item1 is not null); + return item; + } + else + { + item = factory(_database, offset, size, type, objectType, this, injectableValues, network); + bool added = _cache.TryAdd(offset, size, type, item); + } + + return item; + } + + return factory(_database, offset, size, type, objectType, this, injectableValues, network); + } } } diff --git a/MaxMind.Db/Reader.cs b/MaxMind.Db/Reader.cs index 3aec7d84..23c32b95 100644 --- a/MaxMind.Db/Reader.cs +++ b/MaxMind.Db/Reader.cs @@ -121,6 +121,17 @@ public Reader(string file, FileAccessMode mode) { } + /// + /// Initializes a new instance of the class. + /// + /// The MaxMind DB file. + /// The mode by which to access the DB file. + /// Cache size for optional internal cache + public Reader(string file, FileAccessMode mode, int cacheSize) + : this(BufferForMode(file, mode), file, cacheSize) + { + } + /// /// Initialize with Stream. The current position of the /// stream must point to the start of the database. The content @@ -134,7 +145,7 @@ public Reader(Stream stream) : this(new MemoryMapBuffer(stream), null) { } - private Reader(Buffer buffer, string? file) + private Reader(Buffer buffer, string? file, int? cacheSize = null) { _fileName = file; _database = buffer; @@ -146,7 +157,7 @@ private Reader(Buffer buffer, string? file) _nodeByteSize = Metadata.NodeByteSize; _nodeCount = Metadata.NodeCount; _recordSize = Metadata.RecordSize; - Decoder = new Decoder(_database, Metadata.SearchTreeSize + DataSectionSeparatorSize); + Decoder = new Decoder(_database, Metadata.SearchTreeSize + DataSectionSeparatorSize, defaultCacheSize: cacheSize); if (_dbIPVersion == 6) { @@ -163,8 +174,7 @@ private Reader(Buffer buffer, string? file) /// Asynchronously initializes a new instance of the class by reading the specified file into a memory-mapped region. /// /// The file. - /// Optional allocator method for strings created from byte arrays. - public static async Task CreateAsync(string file, AllocatorDelegates.GetString? stringAllocator = null) + public static async Task CreateAsync(string file) { return new Reader(await MemoryMapBuffer.CreateAsync(file).ConfigureAwait(false), file); } @@ -173,9 +183,8 @@ public static async Task CreateAsync(string file, AllocatorDelegates.Get /// Asynchronously initialize with Stream. /// /// The stream to use. It will be used from its current position. - /// Optional allocator method for strings created from byte arrays. /// - public static async Task CreateAsync(Stream stream, AllocatorDelegates.GetString? stringAllocator = null) + public static async Task CreateAsync(Stream stream) { return new Reader(await MemoryMapBuffer.CreateAsync(stream).ConfigureAwait(false), null); } diff --git a/MaxMind.Db/SNEGenericCache.cs b/MaxMind.Db/SNEGenericCache.cs new file mode 100644 index 00000000..0a61c4d8 --- /dev/null +++ b/MaxMind.Db/SNEGenericCache.cs @@ -0,0 +1,94 @@ +namespace MaxMind.Db; + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Threading; +using System.Diagnostics; + +/// +/// Simple non-evicting cache +/// +public class SNEGenericCache +{ + private readonly ConcurrentDictionary<(long, int, Type), (object, long)> _Cache; + + // Long for interlocked operations. Explicitly maintain our own size to avoid + // paying for locking all buckets when alternatively checking this.Cache.Size(). + private ulong _Size; + + private readonly int _Capacity; + private const int DEFAULT_CAPACITY = 4_096; + + /// + /// Simple non-evicting cache + /// + /// + public SNEGenericCache(int maxCapacity = DEFAULT_CAPACITY) + { + this._Cache = new(); + + this._Capacity = maxCapacity; + this._Size = 0; + } + + /// + /// Attempt to add an item to the cache. + /// + /// + /// + /// + /// + /// + public bool TryAdd(long offset, int size, Type type, ValueTuple item) + { + // Try a half fence first, to check if we have hit the cache limits. + if (Volatile.Read(ref this._Size) > (ulong)this._Capacity) + { + return false; + } + + // Half fence came back fine, take the full fence to increment the size. + ulong incrementedValue = Interlocked.Increment(ref this._Size); + + --incrementedValue; + + // Half fence is an optimization, it may read a stale value. Here we know + // that the capacity has been exceeded. Hopefully the next half fence + // read will have propogated the value sufficiently. + if (incrementedValue >= (ulong)this._Capacity) + { + return false; + } + + // Else we can add. Below will most likely end up as a tail call. Do not + // mark the method as aggressive inline. + return this._Cache.TryAdd((offset, size, type), item); + } + + /// + /// Get an object from the cache + /// + /// + /// + /// + /// + /// + public bool TryGet(long offset, int size, Type type, out ValueTuple returnValue) + { + // Read, attempt to return a cached value + if (this._Cache.TryGetValue((offset, size, type), out returnValue)) + { + return true; + } + + // We explicitly use TryGetValue first, in place of GetOrAdd. This is to + // avoid locking on the happy path. + // + // If we have fallen into this logic, we had a cache miss. User must + // attempt a cache add. + + return false; + } + +} \ No newline at end of file From 25ed1f4aabe3ae21b73532e6f0ec20f07ae4fdd9 Mon Sep 17 00:00:00 2001 From: jashook Date: Sun, 1 Mar 2026 20:58:46 -0800 Subject: [PATCH 09/13] Make the cache intense --- MaxMind.Db/SNEGenericCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MaxMind.Db/SNEGenericCache.cs b/MaxMind.Db/SNEGenericCache.cs index 0a61c4d8..32c8571a 100644 --- a/MaxMind.Db/SNEGenericCache.cs +++ b/MaxMind.Db/SNEGenericCache.cs @@ -9,7 +9,7 @@ namespace MaxMind.Db; /// /// Simple non-evicting cache /// -public class SNEGenericCache +internal class SNEGenericCache { private readonly ConcurrentDictionary<(long, int, Type), (object, long)> _Cache; From fb6835fae0011108ac6f8c41b3bcb4cc58f4bb28 Mon Sep 17 00:00:00 2001 From: jashook Date: Sun, 1 Mar 2026 21:17:43 -0800 Subject: [PATCH 10/13] Cleanup --- MaxMind.Db.Benchmark/Program.cs | 52 +------------------ MaxMind.Db/ArrayBuffer.cs | 10 ---- MaxMind.Db/Buffer.cs | 18 ------- MaxMind.Db/Decoder.cs | 2 +- MaxMind.Db/MemoryMapBuffer.cs | 14 ----- .../{SNEGenericCache.cs => SNECache.cs} | 12 ++--- 6 files changed, 8 insertions(+), 100 deletions(-) rename MaxMind.Db/{SNEGenericCache.cs => SNECache.cs} (89%) diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index ca8e03e4..ec46305e 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -292,54 +292,4 @@ public Subdivision( public int? Confidence { get; internal set; } public string? IsoCode { get; internal set; } -} - -#if NETCOREAPP2_1_OR_GREATER - -public sealed class ReadOnlyMemoryByteComparer : IEqualityComparer> - -{ - public static ReadOnlyMemoryByteComparer Default { get; } = new ReadOnlyMemoryByteComparer(); - - public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) - { - return x.Span.SequenceEqual(y.Span); - } - - public int GetHashCode(ReadOnlyMemory obj) - { - unchecked - { - int hash = 17; - ReadOnlySpan span = obj.Span; - - // Simple hashing of the sequence - foreach (byte b in span) - { - hash = hash * 31 + b; - } - return hash; - } - } -} - -public static class InternedStrings -{ - internal static Dictionary, string> s_Dictionary = new (ReadOnlyMemoryByteComparer.Default); - - public static string GetString(ReadOnlyMemory bytes) - { - bool found = s_Dictionary.TryGetValue(bytes, out string? returnValue); - - if (!found) - { - returnValue = Encoding.UTF8.GetString(bytes.Span); - s_Dictionary.TryAdd(bytes, returnValue); - } - - Debug.Assert(returnValue is not null); - return returnValue; - } -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/MaxMind.Db/ArrayBuffer.cs b/MaxMind.Db/ArrayBuffer.cs index 2381ed67..36bfa00d 100644 --- a/MaxMind.Db/ArrayBuffer.cs +++ b/MaxMind.Db/ArrayBuffer.cs @@ -17,16 +17,6 @@ public ArrayBuffer(byte[] array) _fileBytes = array; } - public override ReadOnlySpan AsSpan(long offset, int count) - { - return _fileBytes.AsSpan().Slice((int)offset, count); - } - - public override ReadOnlyMemory AsMemory(long offset, int count) - { - return _fileBytes.AsMemory().Slice((int)offset, count); - } - public override byte[] Read(long offset, int count) { var bytes = new byte[count]; diff --git a/MaxMind.Db/Buffer.cs b/MaxMind.Db/Buffer.cs index 02d8cf90..cf609f97 100644 --- a/MaxMind.Db/Buffer.cs +++ b/MaxMind.Db/Buffer.cs @@ -21,24 +21,6 @@ internal abstract class Buffer : IDisposable public long Length { get; protected set; } - /// - /// Return a slice of the buffer as a read only span. Prefer over the - /// Read method - /// - /// - /// - /// - public abstract ReadOnlySpan AsSpan(long offset, int size); - - /// - /// Return a slice of the buffer as a read only memory. Prefer over the - /// Read method - /// - /// - /// - /// - public abstract ReadOnlyMemory AsMemory(long offset, int size); - /// /// Read a big integer from the buffer. /// diff --git a/MaxMind.Db/Decoder.cs b/MaxMind.Db/Decoder.cs index 37871df3..bcde64ff 100644 --- a/MaxMind.Db/Decoder.cs +++ b/MaxMind.Db/Decoder.cs @@ -48,7 +48,7 @@ internal sealed class Decoder private readonly DictionaryActivatorCreator _dictionaryActivatorCreator; private readonly ListActivatorCreator _listActivatorCreator; - private readonly SNEGenericCache? _cache; + private readonly SNECache? _cache; /// /// Initializes a new instance of the class. diff --git a/MaxMind.Db/MemoryMapBuffer.cs b/MaxMind.Db/MemoryMapBuffer.cs index 0761d3b3..10d24648 100644 --- a/MaxMind.Db/MemoryMapBuffer.cs +++ b/MaxMind.Db/MemoryMapBuffer.cs @@ -245,20 +245,6 @@ private static (MemoryMappedFile File, MemoryMappedViewAccessor View) CreateMmap } } - public override ReadOnlySpan AsSpan(long offset, int count) - { - var bytes = new byte[count]; - _view.ReadArray(offset, bytes, 0, bytes.Length); - return bytes.AsSpan(); - } - - public override ReadOnlyMemory AsMemory(long offset, int count) - { - var bytes = new byte[count]; - _view.ReadArray(offset, bytes, 0, bytes.Length); - return bytes.AsMemory(); - } - public override byte[] Read(long offset, int count) { var bytes = new byte[count]; diff --git a/MaxMind.Db/SNEGenericCache.cs b/MaxMind.Db/SNECache.cs similarity index 89% rename from MaxMind.Db/SNEGenericCache.cs rename to MaxMind.Db/SNECache.cs index 32c8571a..18b27a16 100644 --- a/MaxMind.Db/SNEGenericCache.cs +++ b/MaxMind.Db/SNECache.cs @@ -9,13 +9,13 @@ namespace MaxMind.Db; /// /// Simple non-evicting cache /// -internal class SNEGenericCache +internal class SNECache { private readonly ConcurrentDictionary<(long, int, Type), (object, long)> _Cache; // Long for interlocked operations. Explicitly maintain our own size to avoid // paying for locking all buckets when alternatively checking this.Cache.Size(). - private ulong _Size; + private int _Size; private readonly int _Capacity; private const int DEFAULT_CAPACITY = 4_096; @@ -24,7 +24,7 @@ internal class SNEGenericCache /// Simple non-evicting cache /// /// - public SNEGenericCache(int maxCapacity = DEFAULT_CAPACITY) + public SNECache(int maxCapacity = DEFAULT_CAPACITY) { this._Cache = new(); @@ -43,20 +43,20 @@ public SNEGenericCache(int maxCapacity = DEFAULT_CAPACITY) public bool TryAdd(long offset, int size, Type type, ValueTuple item) { // Try a half fence first, to check if we have hit the cache limits. - if (Volatile.Read(ref this._Size) > (ulong)this._Capacity) + if (Volatile.Read(ref this._Size) > this._Capacity) { return false; } // Half fence came back fine, take the full fence to increment the size. - ulong incrementedValue = Interlocked.Increment(ref this._Size); + int incrementedValue = Interlocked.Increment(ref this._Size); --incrementedValue; // Half fence is an optimization, it may read a stale value. Here we know // that the capacity has been exceeded. Hopefully the next half fence // read will have propogated the value sufficiently. - if (incrementedValue >= (ulong)this._Capacity) + if (incrementedValue >= this._Capacity) { return false; } From ad33f8c52af209569148e1f60a00d41b37b673cf Mon Sep 17 00:00:00 2001 From: jashook Date: Sun, 1 Mar 2026 21:18:11 -0800 Subject: [PATCH 11/13] Comment cleanup --- MaxMind.Db/SNECache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MaxMind.Db/SNECache.cs b/MaxMind.Db/SNECache.cs index 18b27a16..154e3a80 100644 --- a/MaxMind.Db/SNECache.cs +++ b/MaxMind.Db/SNECache.cs @@ -13,8 +13,8 @@ internal class SNECache { private readonly ConcurrentDictionary<(long, int, Type), (object, long)> _Cache; - // Long for interlocked operations. Explicitly maintain our own size to avoid - // paying for locking all buckets when alternatively checking this.Cache.Size(). + // Explicitly maintain our own size to avoid paying for locking all buckets + // when alternatively checking this.Cache.Size(). private int _Size; private readonly int _Capacity; From 1046f57fdab332f627c44e584384ded4d9c483a5 Mon Sep 17 00:00:00 2001 From: jashook Date: Sun, 1 Mar 2026 21:24:16 -0800 Subject: [PATCH 12/13] Fix copy paste issue --- MaxMind.Db/Decoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MaxMind.Db/Decoder.cs b/MaxMind.Db/Decoder.cs index bcde64ff..41e9985e 100644 --- a/MaxMind.Db/Decoder.cs +++ b/MaxMind.Db/Decoder.cs @@ -321,7 +321,7 @@ private byte[] DecodeBytes(Type expectedType, long offset, int size) return (byte[])DecodeFromCacheOrCreate(offset, size, expectedType, static (Buffer database, long offset, int size) => { - return (database.ReadString(offset, size), offset + size); + return (database.Read(offset, size), offset + size); }).Item1; } From 2afe0f885a3010f479bbeef2f453f8a64eaac326 Mon Sep 17 00:00:00 2001 From: jashook Date: Sun, 1 Mar 2026 21:32:18 -0800 Subject: [PATCH 13/13] Expose cache size --- MaxMind.Db/Decoder.cs | 9 +++++++++ MaxMind.Db/Reader.cs | 9 +++++++++ MaxMind.Db/SNECache.cs | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/MaxMind.Db/Decoder.cs b/MaxMind.Db/Decoder.cs index 41e9985e..e03df00b 100644 --- a/MaxMind.Db/Decoder.cs +++ b/MaxMind.Db/Decoder.cs @@ -85,6 +85,15 @@ internal T Decode(long offset, out long outOffset, InjectableValues? injectab return decoded; } + /// + /// Get cache size + /// + /// + internal int CacheSize() + { + return _cache is null ? 0 : _cache.Size(); + } + private object Decode(Type expectedType, long offset, out long outOffset, InjectableValues? injectables = null, Network? network = null) { var type = CtrlData(offset, out var size, out offset); diff --git a/MaxMind.Db/Reader.cs b/MaxMind.Db/Reader.cs index 23c32b95..6b4e2526 100644 --- a/MaxMind.Db/Reader.cs +++ b/MaxMind.Db/Reader.cs @@ -236,6 +236,15 @@ private void Dispose(bool disposing) _disposed = true; } + /// + /// Cache size + /// + /// + public int CacheSize() + { + return Decoder.CacheSize(); + } + /// /// Finds the data related to the specified address. /// diff --git a/MaxMind.Db/SNECache.cs b/MaxMind.Db/SNECache.cs index 154e3a80..caae1d3c 100644 --- a/MaxMind.Db/SNECache.cs +++ b/MaxMind.Db/SNECache.cs @@ -5,6 +5,8 @@ namespace MaxMind.Db; using System.Collections.Concurrent; using System.Threading; using System.Diagnostics; +using System.Drawing; + /// /// Simple non-evicting cache @@ -32,6 +34,8 @@ public SNECache(int maxCapacity = DEFAULT_CAPACITY) this._Size = 0; } + public int Size() => Volatile.Read(ref this._Size); + /// /// Attempt to add an item to the cache. ///