diff --git a/MaxMind.Db.Benchmark/Program.cs b/MaxMind.Db.Benchmark/Program.cs index 0a1cbd32..407e8004 100644 --- a/MaxMind.Db.Benchmark/Program.cs +++ b/MaxMind.Db.Benchmark/Program.cs @@ -4,8 +4,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(); @@ -13,7 +15,12 @@ public class CityBenchmark { // A random IP that has city info. - private Reader _reader = null!; + private Reader _memMapReader = null!; + + private Reader _arrayBufferCachedReader = null!; + private Reader _memMapCachedReader = null!; + private Reader _arrayBufferReader = null!; + private IPAddress[] _ipAddresses = []; [GlobalSetup] @@ -22,7 +29,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); + _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) ?? ""; @@ -46,16 +56,61 @@ public void GlobalSetup() [GlobalCleanup] public void GlobalCleanup() { - _reader.Dispose(); + _memMapReader.Dispose(); + } + + [Benchmark] + public int CityMemoryMappedLookup() + { + int x = 0; + foreach (var ipAddress in _ipAddresses) + { + 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; + } + } + + 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 City() + public int CityMemoryCachedLookup() { int x = 0; foreach (var ipAddress in _ipAddresses) { - if (_reader.Find(ipAddress) != null) + if (_arrayBufferCachedReader?.Find(ipAddress) != null) { x += 1; } @@ -63,6 +118,7 @@ public int City() return x; } + } public abstract class AbstractCountryResponse 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/Decoder.cs b/MaxMind.Db/Decoder.cs index fd575f13..6efdcac2 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 SNECache? _cache; /// /// Initializes a new instance of the class. @@ -54,7 +56,8 @@ internal sealed class Decoder /// The database. /// The base address in the stream. /// Whether to follow pointers. For testing. - internal Decoder(MemoryMapBuffer database, long pointerBase, bool followPointers = true) + /// Whether to use a cache or not. + internal Decoder(Buffer database, long pointerBase, bool followPointers = true, int? defaultCacheSize = null) { _pointerBase = pointerBase; _database = database; @@ -62,6 +65,7 @@ internal Decoder(MemoryMapBuffer database, long pointerBase, bool followPointers _listActivatorCreator = new ListActivatorCreator(); _dictionaryActivatorCreator = new DictionaryActivatorCreator(); _typeActivatorCreator = new TypeActivatorCreator(); + _cache = defaultCacheSize is null ? null : new (defaultCacheSize.Value); } /// @@ -81,10 +85,19 @@ 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); - 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) @@ -145,6 +158,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, @@ -282,14 +327,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.Read(offset, size), offset + size); + }).Item1; } /// @@ -662,5 +713,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/MaxMind.Db.csproj b/MaxMind.Db/MaxMind.Db.csproj index f6789f3b..31533b44 100644 --- a/MaxMind.Db/MaxMind.Db.csproj +++ b/MaxMind.Db/MaxMind.Db.csproj @@ -50,4 +50,7 @@ + + + diff --git a/MaxMind.Db/Reader.cs b/MaxMind.Db/Reader.cs index fcfe75da..24c33706 100644 --- a/MaxMind.Db/Reader.cs +++ b/MaxMind.Db/Reader.cs @@ -110,7 +110,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) { } @@ -119,7 +120,19 @@ 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) + { + } + + /// + /// 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) { } @@ -136,7 +149,7 @@ public Reader(Stream stream) : this(new MemoryMapBuffer(stream), null) { } - private Reader(MemoryMapBuffer buffer, string? file) + private Reader(Buffer buffer, string? file, int? cacheSize = null) { _fileName = file; _database = buffer; @@ -148,7 +161,7 @@ private Reader(MemoryMapBuffer 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) { @@ -227,6 +240,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 new file mode 100644 index 00000000..caae1d3c --- /dev/null +++ b/MaxMind.Db/SNECache.cs @@ -0,0 +1,98 @@ +namespace MaxMind.Db; + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Threading; +using System.Diagnostics; +using System.Drawing; + + +/// +/// Simple non-evicting cache +/// +internal class SNECache +{ + private readonly ConcurrentDictionary<(long, int, Type), (object, long)> _Cache; + + // 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; + private const int DEFAULT_CAPACITY = 4_096; + + /// + /// Simple non-evicting cache + /// + /// + public SNECache(int maxCapacity = DEFAULT_CAPACITY) + { + this._Cache = new(); + + this._Capacity = maxCapacity; + this._Size = 0; + } + + public int Size() => Volatile.Read(ref this._Size); + + /// + /// 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) > this._Capacity) + { + return false; + } + + // Half fence came back fine, take the full fence to increment the 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 >= 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