【问题标题】:Fastest C# collection for searching by property按属性搜索的最快 C# 集合
【发布时间】:2014-04-14 22:51:29
【问题描述】:

我有以下简单的类:

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 之间的行。范围不重叠,但可能是稀疏的。我发现执行此操作的最简单方法如下:

public MyClass Lookup(long value){
    return _set.FirstOrDefault(d => value >= d.StartRange && value <= d.EndRange);
}

我已经尝试过将集合存储为IOrderedEnumerable&lt;T&gt;SortedSet&lt;T&gt;。 SortedSet 快了一个数量级。 HashSet&lt;T&gt; 比 SortedSet 稍微快一点。任何关于使用最有效的集合类或更好的查找的建议将不胜感激。

【问题讨论】:

  • 尝试查看this article。它很好地解释了每个集合类的工作原理。
  • 使用struct 而不是class(并使其不可变)。 10^6 乘以 ( 8 + 8 + 4 ) 字节为 20 MB,但类是两倍。然后将它们存储在排序数组中并使用二进制搜索,如下所述。
  • @KrisVandermotten:我很难找到有关对象引用的空间要求的信息。您能否为评论中的“课程数量增加一倍”部分提供引用?
  • @StriplingWarrior msdn.microsoft.com/en-us/magazine/cc163791.aspx 上的文章已经很老了,细节可能已经改变(例如支持 64 位),但请查看标题为“ObjectInstance”的部分。它归结为:堆上的对象有内存管理和垃圾收集的开销,支持继承等。结构没有这个开销。

标签: c# search optimization collections


【解决方案1】:

范围不重叠,但可能是稀疏的。

如果我理解正确,这意味着如果你按 StartRange 对它们进行排序,然后用value &gt;= d.StartRange 标识第一个项目,你可以立即知道你找到了你的项目(如果value &lt;= 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;
    };
}

【讨论】:

  • 感谢您的回答。我认为您的意思是 LastOrDefault 用于排序列表。请为未来的读者提及一些关于 struct 的内容。非常感谢。
  • @TheGwa: 不,我很确定我的意思是FirstOrDefault(),因为我希望它从头开始寻找,直到找到StartRange 出现在(或等于)给定值之前的那个价值。然后我希望它短路并避免搜索其余部分。而且,关于struct,就我而言,陪审团仍然在那个问题上。我无法找到足够的信息,也无法进行足够好的测试,以确定这是否是一个好主意。
【解决方案2】:

你能不能只按StartRange 排序,使用Array.BinarySearch 来找到最近的一个(仍然更小),因为你的范围很稀疏,通过一次检查就知道(如果Endrange 大于x)如果你找到了还是错过了?
你所要做的就是实现IComparable&lt;T&gt;StartRange 作为键,这很容易。

【讨论】:

  • 你在正确的轨道上,但它并不那么容易。除非您正在寻找完全相同的 StartRange 值,否则 BinarySearch 方法不会帮助您找到“最近的”,不是吗?
  • Array.BinarySearch 应该返回最接近的匹配,并带有一些 bitmagic。(msdn.microsoft.com/en-us/library/2cy9f6wb%28v=vs.110%29.aspx) 否则您必须实现自己的 BinSearch,它会返回最接近的匹配(这也很容易。)
【解决方案3】:

可能不是您真正需要的,但您研究过 NoSQL 吗? 有些实现就像你想要的那样,有些有一个可以从内存中运行的缓存,所以应该很快。

如果我没记错的话,Redis 可能是您想要研究的那个。 (这是关于 Redis 的 article)。

K,对我的东西进行了一些挖掘:NoSQL DB comparisson,你可以看到有哪些主要风格以及它们适用于哪些用例。

据我所知,他们在一些实现中使用 B-Trees 来提高速度。如果你想重新创建轮子,我猜你可能想做类似的事情。

【讨论】:

  • 我们已将其用于其他解决方案。尤其是 Couchbase,但是对于一个非常简单的缓存来说有相当多的开销。
  • 完全同意,但仍然指出了替代方案。
【解决方案4】:

如果您的范围现在或将来可能重叠,您可以考虑使用interval tree

但是,如果您确实确定您的范围没有重叠,请将您的 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 ;

    }

}

【讨论】:

    猜你喜欢
    • 2023-01-25
    • 2010-11-03
    • 2014-10-03
    • 2011-12-30
    • 2013-01-16
    • 1970-01-01
    • 2014-02-18
    • 2013-09-13
    • 2017-03-20
    相关资源
    最近更新 更多