Merge pull request #401 from ArgoZhang/dev-net

fix(NET): refactor GetData method
This commit is contained in:
Leon / 狮子的魂 2025-11-24 16:07:01 +08:00 committed by GitHub
commit 096d68ea77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 190 additions and 105 deletions

View File

@ -2,7 +2,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [Unreleased] ## [3.0.0] - 2025-11-22
- 支持 .NET 10.0
- 增加 IPv6 支持
- 修复若干 bug
## [2.0.1] - 2023-07-30 ## [2.0.1] - 2023-07-30
@ -18,4 +21,4 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Dependent file query policies CachePolicy.VectorIndex, CachePolicy.File support thread-safe concurrent queries - Dependent file query policies CachePolicy.VectorIndex, CachePolicy.File support thread-safe concurrent queries
- Dramatically optimizes overall performance - Dramatically optimizes overall performance

View File

@ -10,6 +10,7 @@ using IP2Region.Net.XDB;
namespace IP2Region.Net.BenchMark; namespace IP2Region.Net.BenchMark;
[MemoryDiagnoser]
public class Benchmarks public class Benchmarks
{ {
private static readonly string XdbPathV4 = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v4.xdb"); private static readonly string XdbPathV4 = Path.Combine(AppContext.BaseDirectory, "IP2Region", "ip2region_v4.xdb");
@ -24,6 +25,17 @@ public class Benchmarks
private readonly string _testIPv4Address = "114.114.114.114"; private readonly string _testIPv4Address = "114.114.114.114";
private readonly string _testIPv6Address = "240e:3b7:3272:d8d0:db09:c067:8d59:539e"; private readonly string _testIPv6Address = "240e:3b7:3272:d8d0:db09:c067:8d59:539e";
public Benchmarks()
{
_contentV4Searcher.Search(_testIPv4Address);
_vectorV4Searcher.Search(_testIPv4Address);
_fileV4Searcher.Search(_testIPv4Address);
_contentV6Searcher.Search(_testIPv6Address);
_vectorV6Searcher.Search(_testIPv6Address);
_fileV6Searcher.Search(_testIPv6Address);
}
[Benchmark] [Benchmark]
[BenchmarkCategory("IPv4")] [BenchmarkCategory("IPv4")]
public void ContentIPv4() => _contentV4Searcher.Search(_testIPv4Address); public void ContentIPv4() => _contentV4Searcher.Search(_testIPv4Address);

View File

@ -78,6 +78,8 @@ public class SearcherTest
searcher.Search("58.251.27.201"); searcher.Search("58.251.27.201");
Assert.Equal(3, searcher.IoCount); Assert.Equal(3, searcher.IoCount);
searcher.Dispose();
} }
[Fact] [Fact]
@ -89,6 +91,8 @@ public class SearcherTest
searcher.Search("58.251.27.201"); searcher.Search("58.251.27.201");
Assert.Equal(2, searcher.IoCount); Assert.Equal(2, searcher.IoCount);
searcher.Dispose();
} }
[Fact] [Fact]
@ -100,6 +104,8 @@ public class SearcherTest
searcher.Search("58.251.27.201"); searcher.Search("58.251.27.201");
Assert.Equal(0, searcher.IoCount); Assert.Equal(0, searcher.IoCount);
searcher.Dispose();
} }
[Theory] [Theory]
@ -110,10 +116,6 @@ public class SearcherTest
var ipAddress = IPAddress.Parse(ipStr); var ipAddress = IPAddress.Parse(ipStr);
var region = fileSearcher.Search(ipAddress); var region = fileSearcher.Search(ipAddress);
Assert.Equal(expected, region); Assert.Equal(expected, region);
var ip = XDB.Util.IpAddressToUInt32(ipAddress);
region = fileSearcher.Search(ip);
Assert.Equal(expected, region);
} }
[Theory] [Theory]

View File

@ -12,7 +12,7 @@ namespace IP2Region.Net.Abstractions;
/// <summary> /// <summary>
/// IP 转化为地理位置搜索器接口 /// IP 转化为地理位置搜索器接口
/// </summary> /// </summary>
public interface ISearcher public interface ISearcher : IDisposable
{ {
/// <summary> /// <summary>
/// 搜索方法 /// 搜索方法
@ -30,6 +30,7 @@ public interface ISearcher
/// 搜索方法 仅限 IPv4 使用 /// 搜索方法 仅限 IPv4 使用
/// </summary> /// </summary>
/// <param name="ipAddress">IPv4 地址字节数组小端读取 uint 数值</param> /// <param name="ipAddress">IPv4 地址字节数组小端读取 uint 数值</param>
[Obsolete("已弃用请改用其他方法Deprecated; please use Search(string) or Search(IPAddress) method.")]
string? Search(uint ipAddress); string? Search(uint ipAddress);
/// <summary> /// <summary>

View File

@ -2,9 +2,9 @@
<PropertyGroup> <PropertyGroup>
<id>IP2Region.Net</id> <id>IP2Region.Net</id>
<version>2.1.1</version> <version>3.0.0</version>
<title>IP2Region.Net</title> <title>IP2Region.Net</title>
<authors>Alan Lee</authors> <authors>Alan Lee;Argo Zhang(argo@live.ca)</authors>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/lionsoul2014/ip2region/tree/master/binding/csharp</PackageProjectUrl> <PackageProjectUrl>https://github.com/lionsoul2014/ip2region/tree/master/binding/csharp</PackageProjectUrl>

View File

@ -1,54 +0,0 @@
// Copyright 2025 The Ip2Region Authors. All rights reserved.
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
// @Author Alan <lzh.shap@gmail.com>
// @Date 2023/07/25
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
using System.Buffers;
namespace IP2Region.Net.Internal.Abstractions;
internal abstract class AbstractCacheStrategy(string xdbPath)
{
protected const int HeaderInfoLength = 256;
protected const int VectorIndexSize = 8;
private const int BufferSize = 64 * 1024;
public int IoCount { get; private set; }
protected FileStream XdbFileStream = new(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.RandomAccess);
public void ResetIoCount()
{
IoCount = 0;
}
public virtual ReadOnlyMemory<byte> GetVectorIndex(int offset) => GetData(HeaderInfoLength + offset, VectorIndexSize);
public virtual ReadOnlyMemory<byte> GetData(long offset, int length)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);
int totalBytesRead = 0;
try
{
XdbFileStream.Seek(offset, SeekOrigin.Begin);
int bytesRead;
while (totalBytesRead < length)
{
bytesRead = XdbFileStream.Read(buffer, totalBytesRead, length);
totalBytesRead += bytesRead;
IoCount++;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return new ReadOnlyMemory<byte>(buffer, 0, totalBytesRead);
}
}

View File

@ -5,14 +5,13 @@
// @Date 2023/07/25 // @Date 2023/07/25
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21 // Updated by Argo Zhang <argo@live.ca> at 2025/11/21
using IP2Region.Net.Internal.Abstractions;
using IP2Region.Net.XDB; using IP2Region.Net.XDB;
namespace IP2Region.Net.Internal; namespace IP2Region.Net.Internal;
class CacheStrategyFactory(string xdbPath) static class CacheStrategyFactory
{ {
public AbstractCacheStrategy CreateCacheStrategy(CachePolicy cachePolicy) => cachePolicy switch public static ICacheStrategy CreateCacheStrategy(CachePolicy cachePolicy, string xdbPath) => cachePolicy switch
{ {
CachePolicy.Content => new ContentCacheStrategy(xdbPath), CachePolicy.Content => new ContentCacheStrategy(xdbPath),
CachePolicy.VectorIndex => new VectorIndexCacheStrategy(xdbPath), CachePolicy.VectorIndex => new VectorIndexCacheStrategy(xdbPath),

View File

@ -5,11 +5,9 @@
// @Date 2023/07/25 // @Date 2023/07/25
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21 // Updated by Argo Zhang <argo@live.ca> at 2025/11/21
using IP2Region.Net.Internal.Abstractions;
namespace IP2Region.Net.Internal; namespace IP2Region.Net.Internal;
class ContentCacheStrategy : AbstractCacheStrategy class ContentCacheStrategy : FileCacheStrategy
{ {
private readonly ReadOnlyMemory<byte> _cacheData; private readonly ReadOnlyMemory<byte> _cacheData;
@ -20,5 +18,10 @@ class ContentCacheStrategy : AbstractCacheStrategy
XdbFileStream.Dispose(); XdbFileStream.Dispose();
} }
public override ReadOnlyMemory<byte> GetData(long offset = 0, int length = 0) => _cacheData.Slice((int)offset, length); public override ReadOnlyMemory<byte> GetData(long offset, int length) => _cacheData.Slice((int)offset, length);
protected override void Dispose(bool disposing)
{
base.Dispose(false);
}
} }

View File

@ -5,11 +5,81 @@
// @Date 2023/07/25 // @Date 2023/07/25
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21 // Updated by Argo Zhang <argo@live.ca> at 2025/11/21
using IP2Region.Net.Internal.Abstractions; using System.Buffers;
namespace IP2Region.Net.Internal; namespace IP2Region.Net.Internal;
class FileCacheStrategy(string xdbPath) : AbstractCacheStrategy(xdbPath) class FileCacheStrategy(string xdbPath) : ICacheStrategy
{ {
protected const int HeaderInfoLength = 256;
protected const int VectorIndexSize = 8;
protected const int BufferSize = 64 * 1024;
protected FileStream XdbFileStream = new(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.RandomAccess);
public int IoCount { get; set; }
public void ResetIoCount()
{
IoCount = 0;
}
public virtual ReadOnlyMemory<byte> GetVectorIndex(int offset) => GetData(HeaderInfoLength + offset, VectorIndexSize);
public virtual ReadOnlyMemory<byte> GetData(long offset, int length)
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
int totalBytesRead = 0;
XdbFileStream.Seek(offset, SeekOrigin.Begin);
int bytesRead;
while (totalBytesRead < length)
{
bytesRead = XdbFileStream.Read(buffer, totalBytesRead, length - totalBytesRead);
if (bytesRead == 0)
{
break;
}
totalBytesRead += bytesRead;
IoCount++;
}
var ret = new byte[totalBytesRead];
if (totalBytesRead > 0)
{
Array.Copy(buffer, 0, ret, 0, totalBytesRead);
}
return ret;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// 释放文件句柄
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
XdbFileStream.Close();
XdbFileStream.Dispose();
}
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
} }

View File

@ -0,0 +1,19 @@
// Copyright 2025 The Ip2Region Authors. All rights reserved.
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
// @Author Alan <lzh.shap@gmail.com>
// @Date 2023/07/25
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21
namespace IP2Region.Net.Internal;
internal interface ICacheStrategy : IDisposable
{
int IoCount { get; }
void ResetIoCount();
ReadOnlyMemory<byte> GetVectorIndex(int offset);
ReadOnlyMemory<byte> GetData(long offset, int length);
}

View File

@ -5,11 +5,9 @@
// @Date 2023/07/25 // @Date 2023/07/25
// Updated by Argo Zhang <argo@live.ca> at 2025/11/21 // Updated by Argo Zhang <argo@live.ca> at 2025/11/21
using IP2Region.Net.Internal.Abstractions;
namespace IP2Region.Net.Internal; namespace IP2Region.Net.Internal;
class VectorIndexCacheStrategy : AbstractCacheStrategy class VectorIndexCacheStrategy : FileCacheStrategy
{ {
private const int VectorIndexRows = 256; private const int VectorIndexRows = 256;
private const int VectorIndexCols = 256; private const int VectorIndexCols = 256;
@ -18,10 +16,7 @@ class VectorIndexCacheStrategy : AbstractCacheStrategy
public VectorIndexCacheStrategy(string xdbPath) : base(xdbPath) public VectorIndexCacheStrategy(string xdbPath) : base(xdbPath)
{ {
XdbFileStream.Seek(HeaderInfoLength, SeekOrigin.Begin); _vectorCache = GetData(HeaderInfoLength, VectorIndexRows * VectorIndexCols * VectorIndexSize);
var buffer = new byte[VectorIndexRows * VectorIndexCols * VectorIndexSize];
var length = XdbFileStream.Read(buffer, 0, buffer.Length);
_vectorCache = new ReadOnlyMemory<byte>(buffer);
} }
public override ReadOnlyMemory<byte> GetVectorIndex(int offset) => _vectorCache.Slice(offset, VectorIndexSize); public override ReadOnlyMemory<byte> GetVectorIndex(int offset) => _vectorCache.Slice(offset, VectorIndexSize);

View File

@ -7,8 +7,8 @@
using IP2Region.Net.Abstractions; using IP2Region.Net.Abstractions;
using IP2Region.Net.Internal; using IP2Region.Net.Internal;
using IP2Region.Net.Internal.Abstractions;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using System.Text; using System.Text;
@ -17,24 +17,18 @@ namespace IP2Region.Net.XDB;
/// <summary> /// <summary>
/// <see cref="ISearcher"/> 实现类 /// <see cref="ISearcher"/> 实现类
/// </summary> /// </summary>
public class Searcher : ISearcher /// <remarks>
/// <inheritdoc/>
/// </remarks>
public class Searcher(CachePolicy cachePolicy, string xdbPath) : ISearcher
{ {
private readonly AbstractCacheStrategy _cacheStrategy; private readonly ICacheStrategy _cacheStrategy = CacheStrategyFactory.CreateCacheStrategy(cachePolicy, xdbPath);
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
public int IoCount => _cacheStrategy.IoCount; public int IoCount => _cacheStrategy.IoCount;
/// <summary>
/// <inheritdoc/>
/// </summary>
public Searcher(CachePolicy cachePolicy, string dbPath)
{
var factory = new CacheStrategyFactory(dbPath);
_cacheStrategy = factory.CreateCacheStrategy(cachePolicy);
}
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
@ -47,14 +41,13 @@ public class Searcher : ISearcher
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
public string? Search(IPAddress ipAddress) public string? Search(IPAddress ipAddress) => SearchCore(ipAddress.GetAddressBytes());
{
return SearchCore(ipAddress.GetAddressBytes());
}
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
[Obsolete("已弃用请改用其他方法Deprecated; please use Search(string) or Search(IPAddress) method.")]
[ExcludeFromCodeCoverage]
public string? Search(uint ipAddress) public string? Search(uint ipAddress)
{ {
var bytes = BitConverter.GetBytes(ipAddress); var bytes = BitConverter.GetBytes(ipAddress);
@ -93,7 +86,7 @@ public class Searcher : ISearcher
{ {
int m = (int)(l + h) >> 1; int m = (int)(l + h) >> 1;
var p = (int)sPtr + m * indexSize; var p = sPtr + m * indexSize;
var buff = _cacheStrategy.GetData(p, indexSize); var buff = _cacheStrategy.GetData(p, indexSize);
var s = buff.Span.Slice(0, length); var s = buff.Span.Slice(0, length);
@ -114,7 +107,7 @@ public class Searcher : ISearcher
} }
} }
var regionBuff = _cacheStrategy.GetData((int)dataPtr, dataLen); var regionBuff = _cacheStrategy.GetData(dataPtr, dataLen);
return Encoding.UTF8.GetString(regionBuff.Span.ToArray()); return Encoding.UTF8.GetString(regionBuff.Span.ToArray());
} }
@ -154,4 +147,13 @@ public class Searcher : ISearcher
} }
return ret; return ret;
} }
/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
_cacheStrategy.Dispose();
GC.SuppressFinalize(this);
}
} }

View File

@ -24,6 +24,7 @@ ISearcher searcher = new Searcher(CachePolicy , "your xdb file path");
| CachePolicy.Content | Cache the entire `xdb` data. | Yes | | CachePolicy.Content | Cache the entire `xdb` data. | Yes |
| CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes | | CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes |
| CachePolicy.File | Completely file-based queries | Yes | | CachePolicy.File | Completely file-based queries | Yes |
### XDB File Description ### XDB File Description
Generate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files Generate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files
@ -47,15 +48,47 @@ provider.GetRequiredKeyedService<ISearcher>("IP2Region.Net");
netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0 netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0
## Performance ## Performance
// * Summary *
| Method | Mean | Error | StdDev | Median | BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171)
|------------ |------------:|----------:|------------:|------------:| 13th Gen Intel Core i7-13700 2.10GHz, 1 CPU, 24 logical and 16 physical cores
| ContentIPv4 | 101.5 ns | 2.04 ns | 4.57 ns | 103.1 ns | .NET SDK 10.0.100
| VectorIPv4 | 7,488.1 ns | 222.91 ns | 657.26 ns | 7,819.2 ns | [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3
| FileIPv4 | 11,686.9 ns | 59.81 ns | 55.95 ns | 11,707.6 ns | DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3
| ContentIPv6 | 296.1 ns | 1.84 ns | 1.72 ns | 296.2 ns |
| VectorIPv6 | 15,025.1 ns | 938.17 ns | 2,766.21 ns | 16,642.9 ns |
| FileIPv6 | 19,721.0 ns | 807.55 ns | 2,381.08 ns | 20,905.7 ns | | Method | Mean | Error | StdDev | Gen0 | Allocated |
|------------ |-------------:|-----------:|-----------:|-------:|----------:|
| ContentIPv4 | 53.70 ns | 0.296 ns | 0.277 ns | 0.0086 | 136 B |
| VectorIPv4 | 4,446.04 ns | 18.673 ns | 15.593 ns | 0.0076 | 232 B |
| FileIPv4 | 6,712.40 ns | 15.718 ns | 13.934 ns | 0.0153 | 264 B |
| ContentIPv6 | 145.53 ns | 0.331 ns | 0.277 ns | 0.0126 | 200 B |
| VectorIPv6 | 7,058.39 ns | 125.505 ns | 117.398 ns | 0.0381 | 712 B |
| FileIPv6 | 10,657.97 ns | 53.907 ns | 50.425 ns | 0.0458 | 744 B |
// * Hints *
Outliers
Benchmarks.VectorIPv4: Default -> 2 outliers were removed (4.55 us, 4.58 us)
Benchmarks.FileIPv4: Default -> 1 outlier was removed (6.79 us)
Benchmarks.ContentIPv6: Default -> 2 outliers were removed (148.08 ns, 152.27 ns)
// * Legends *
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Gen0 : GC Generation 0 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 ns : 1 Nanosecond (0.000000001 sec)
// * Diagnostic Output - MemoryDiagnoser *
// ***** BenchmarkRunner: End *****
Run time: 00:02:06 (126.09 sec), executed benchmarks: 6
Global total time: 00:02:13 (133.47 sec), executed benchmarks: 6
// * Artifacts cleanup *
Artifacts cleanup is finished
## Contributing ## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.