按属性搜索的最快C#集合
本文关键字:集合 属性 搜索 | 更新日期: 2023-09-27 17:58:15
我有以下简单的类:
public class MyClass{
public long StartRange { get; set; }
public long EndRange { get; set; }
public int Id { get; set; }
}
我需要在内存缓存中存储很多,10^5到10^6。在应用程序启动时,将有一次对此缓存的写入和多次读取。此缓存将在ASP.NET环境中访问,因为有很多线程。
我需要在此缓存中查找一行,其中我的值介于StartRange和EndRange之间(包括StartRange和EndRange)。范围不重叠,但可能是稀疏的。我找到的最简单的方法是:
public MyClass Lookup(long value){
return _set.FirstOrDefault(d => value >= d.StartRange && value <= d.EndRange);
}
我已经尝试过将集合存储为IOrderedEnumerable<T>
和SortedSet<T>
。SortedSet的速度要快一个数量级。HashSet<T>
在某种程度上比SortedSet稍快。任何关于使用最有效的集合类或更好的查找的建议都将不胜感激。
范围不重叠,但可能是稀疏的。
如果我理解正确,这意味着如果你按StartRange对它们进行排序,然后用value >= d.StartRange
识别第一个项目,你可以立即知道你已经找到了你的项目(如果是value <= d.EndRange
),或者没有匹配项,对吧?
所以你可以通过这样做将你的时间减半:
public MyClass Lookup(long value){
var candidate = _set.FirstOrDefault(d => value >= d.StartRange);
if(candidate != null && value <= candidate.EndRange)
{
return candidate;
}
return null;
}
而且,由于在排序集合中搜索可以在O(log n)
时间内轻松完成,因此只需进行二进制搜索就可以获得显著的性能提升。以下是一些示例代码,这些代码应该会让您步入正轨。
List<MyClass> _set = new[]{
new MyClass{StartRange = 18, EndRange = 18},
new MyClass{StartRange = 10, EndRange = 15},
new MyClass{StartRange = 20, EndRange = 21}
}.OrderBy(m => m.StartRange).ToList();
public class StartRangeComparer : IComparer<MyClass>
{
public int Compare(MyClass first, MyClass second)
{
return first.StartRange.CompareTo(second.StartRange);
}
}
StartRangeComparer startRangeComparer = new StartRangeComparer();
public MyClass Lookup(long value){
var index = _set.BinarySearch(new MyClass{StartRange = value}, startRangeComparer);
int candidateIndex = index >= 0 ? index : (~index) - 1;
if(candidateIndex < 0)
{
// the given value is before any start-ranges in the list
return null;
}
MyClass candidate = _set[candidateIndex];
if(candidate.EndRange >= value)
{
return candidate;
}
else
{
return null;
};
}
您是否可以不按StartRange
排序,使用Array.BinarySearch
查找最近的一个(仍然较小),并且由于您的范围很稀疏,只需一次检查(如果Endrange
大于x)就可以知道您是否找到了一个或遗漏了一个
您所要做的就是以StartRange
为密钥来实现IComparable<T>
,这很容易。
可能不是您真正需要的,但您研究过NoSQL吗?有些实现就像您想要的一样,有些实现有一个可以从内存中运行的缓存,所以应该很快。
如果我没记错的话,Redis可能是你想要调查的。(这是一篇关于Redis的文章)。
K、 对我的东西进行了一些挖掘:NoSQL DB comparison,你可以在那里看到主要的风格以及它们适合哪些用例。
据我所见,他们在一些实现中使用B树来提高速度。如果你想重新创建轮子,我想你可能想做一些类似的事情。
如果您的范围现在或将来可能重叠,您可以考虑使用间隔树。
但是,如果你真的确定你的范围没有重叠,那么把你的class
改为struct
,然后做下面的事情。将class
更改为struct
的原因有两个:
-
使用数组保存内存。引用类型数组是对堆上单个对象的引用数组。
-
可能更重要的是,它有助于保留引用的位置。您将在一个连续的内存块中跳来跳去,而不是在整个堆中随机跳去。这应该有助于减少寻呼。
这是代码:
class MyClassMap
{
MyClass[] backingStore ; // ordered by StartRange, then EndRange
public MyClassMap( IEnumerable<MyClass> source )
{
backingStore.OrderBy( x => x.StartRange ).ThenBy( x => x.EndRange ) ;
}
public int? GetIdFromValue( long value )
{
int lo = 0 ;
int hi = backingStore.Length ;
int? ix = null ;
while ( lo <= hi && !ix.HasValue )
{
int mid = lo + ((hi-lo)>>1) ;
MyClass current = backingStore[mid] ;
if ( value > current.EndRange ) { lo = mid+1 ; }
else if ( value < current.StartRange ) { hi = mid-1 ; }
else { ix = current.Id ; }
}
return ix ;
}
}