【问题标题】:Collection with very fast iterating and good addition and remove speeds具有非常快的迭代和良好的添加和删除速度的集合
【发布时间】:2012-02-04 09:12:02
【问题描述】:

我正在寻找一个可以快速迭代的集合。我还将相当定期地添加项目和删除(特定)项目,因此理想情况下也希望这些操作也很快。

我正在 xbox 上开发,因此受限于紧凑的框架(或多或少)。将垃圾和对象分配保持在最低限度是非常重要的,所以我可以为我的对象预先分配空间的任何东西都会很棒。

我将在集合中存储uints(但如果需要,可以是ints)。不过,一个通用的解决方案会很好,因为我相信我将来会有需要。

.net 集合将是理想的,但轻量级和开源的东西会很棒。

是否有适合我需要的集合类?如果没有,我将如何创建一个?


更详细地说,它们是类应该处理每一帧的对象 id。它们通常会按升序添加,有间隙。没有上限。但是,可以删除任何内容,这会留下空白。
迭代顺序并不完全重要,但如果它的顺序一致,它会非常有用(特别是对于调试)。

【问题讨论】:

  • HashSet<uint> 怎么样?不知道这是否在紧凑的框架中。但它的迭代速度应该相当快,并且可以快速添加/删除项目。由于它支持数组,因此它的分配很少。 “但在相同的值下必须相同”可能有点问题,因为我认为 HashSet<T> 不能保证这一点,即使在实践中可能会这样。
  • CF里没有,这是我问的主要原因。检查 MSDN 上的构造函数图标是我通常如何判断它是否存在的方式(将 HashSet<T> 的构造函数图标与 List<T> 的图标(受支持)进行比较。
  • HashSet<T> 似乎不属于紧凑框架。
  • 从内存分配的角度来看,基于数组的解决方案似乎是最好的,但使用链表类型的方法添加和删除会更快。您可能需要决定哪种情况更重要。
  • @SomeWritesReserved:例如如果我每帧创建一个 0.5MB 大的对象,并将其添加到持久列表中,我会每 2 帧进行一次垃圾收集(实际上不会收集任何东西)。

标签: c# performance collections xna xbox


【解决方案1】:

我有两个建议可以尝试:

首先,如何使用 Reflector 或 ILSpy 来查看 Generic List<T> 内部?我假设实现不在 CF 中,或者您可以使用它。 Generic List<T> 支持数组,并使用从长度为 4 的数组开始的加倍算法,每次调用 .Add 超过容量都会使其加倍并执行 Array.Copy 到新数组中。除非您将容量显式设置为零,否则它不会调整大小,因此从内存的角度要小心。添加是一回事,但每次删除都会导致数组在被删除的索引之后被复制和左移。

第二个建议是 - 创建一个自定义类如何包装一个整数数组来处理这个问题,它使用与通用List<T>(处理调整大小)类似的双算法(或四倍),但从说大小 256 开始. 您可以随心所欲地将对象整数 id 添加到此乱序中,并且它不会经常翻倍。您还可以通过将 (int)-1 或 uint.MaxValue 指定为“空索引”来模拟删除,这意味着删除时没有 Array.Copy。然后,在绘制之前对每帧应用某种快速排序以对对象索引数组进行排序。如果您按升序排序,所有 -1 将出现在开头(或 uint.MaxValues 在结尾)并且可以忽略。您可以通过在单独的线程上调整和删除前面的-1 来定期“收集”索引数组(注意 - 使用锁定;)

你怎么看?只是认为您将每帧抵消一些计算以进行快速排序(在 xbox 上并不昂贵,而在每个 Remove 和一些 Adds 上的内存分配/收集(昂贵)。

UPDATE - BlockArray - List 其中 T 是固定大小的数组

对此有进一步的评论。我最近正在试验动态大小列表的最有效数据结构,并通过实验发现数组块 - 一个由 T[] 列表支持的类,其中每个 T[] 是一个固定大小块的数组,例如512、4096、8192 与普通 List 相比有几个优点。

我发现 List 中的 Add()(大小未知)的实现大大优于 List 的 Add(),尤其是当总大小变大时。这是由于 List 的加倍算法要求新数组的大小是旧数组的 2 倍,而当超过大小时,memcpy 会超过旧数组。

迭代速度相似。对于小块大小 (512),预分配(预定义容量)比 List 慢得多,但对于大块大小 (8192) 仅稍慢一些。移除是有问题的,因为需要复制/左移许多块。

有趣的是在 List(块列表)中,您可以缓存/池化块。如果足够小,T[] 块适合小对象堆(有利于压缩,更快分配),可能适合 L2 缓存,并且可以预先分配和池化一些块

【讨论】:

  • 我真的很喜欢使用列表的想法,只是让元素失效,以便以后删除。我认为在迭代时我会跳过无效的,所以我不需要排序,然后当我发现无效的元素时,我会定期将所有元素向下移动以有效地删除它们。
  • @GeorgeDuckett 谢谢!是的,您需要进行排序,以便按渲染顺序获取场景图。此解决方案的优点是您可以只添加到数组的末尾(无序),而无需对每个添加和删除执行排序,只需将数组值设置为 -1。也许为了提高性能,不要在数组中搜索对象索引 Id = 123,而是将索引也存储到对象上的 IndexArray 中?只是要真正尝试的事情。 .NET 是一种功能强大的语言,在此之前我用 C# 编写了高性能的模拟软件。只需要通过减少 GC 来挤出周期
  • ... 我应该补充一点,C# 中的性能增强与 C++ 中的相同。如果您编写本地代码 new() new() new() delete delete delete 如果它很慢,您不会感到惊讶,我想知道为什么人们觉得 .NET 应该有所不同? ;-P
【解决方案2】:

使用 Mono 的HashSet<T> 怎么样?

https://github.com/mono/mono/blob/master/mcs/class/System.Core/System.Collections.Generic/HashSet.cs

它在 MIT X11 下获得许可,这是一个许可许可。


根据T 实现GetHashCode 的方式,迭代顺序可能是个问题,但在使用uint 时这应该不是问题。而GetHashCode 的默认实现在不同的实例上返回不同的值,这可能导致不同的迭代顺序。

迭代顺序也可能取决于添加的订单项。即,如果以不同的顺序添加项目,则包含相同项目的两个集合可能会以不同的顺序迭代。

还有一个SortedSet<T>集合,但不知道它有什么性能特点。

【讨论】:

    【解决方案3】:

    自定义类 SparseList 怎么样:

    • 一个列表,允许您通过将缺失值设置为 null 来删除项目(或者对于 SparseValueList,一个特殊的值,如 -1(如 Dr ABT 的解决方案)、0(默认值 (T))、或 int.MinValue, 或 uint.MaxValue,) 和
    • 然后维护已删除索引列表(堆栈)。然后,当您需要添加到列表中时,它会从堆栈中弹出一个已删除的索引并在那里添加值。 (对于多线程,也许 ConcurrentQueue 会是另一个想法。)
    • 枚举器可以跳过已删除的项目(以支持 foreach
    • 可以在枚举期间从集合中删除项目! (我必须经常这样做,所以这很好。)
    • 索引器提供对包含空值的列表的原始访问。所以如果您使用 for(;;) 循环,请准备好过滤掉空值
    • 如果/当您想删除所有空值时,请调用 Compact()
    • 如果在游戏中从不调用 compact,我担心会遍历大量空值。因此,对于紧凑的实验性替代方案,请参阅 SparseListCleaningEnumerator: auto-shrink the list every time it is enumerated, 至少对于单线程情况:当 MoveNext 离开一个项目时,它会查看堆栈以查看索引是否较低并且如果是这样,它将项目分配给较低的索引,将其从当前位置删除,这将缩小列表。平衡可能需要多次迭代并在优化之前涉及多次移动,除非堆栈被排序列表替换,或者堆栈偶尔排序。如果最后一个值为 null,这将不起作用,因为索引将被埋在空闲索引堆栈中(用排序的东西替换堆栈可以避免这种情况)。

    我实现了这个(尚未测试),但我存储的是对我的游戏实体的实际引用而不是 id,因此您需要以某种方式将其调整为 int 或 Nullable。 (好的,以确保我回答您的问题的 int/uint 要求,我还添加了一个略有不同的 SparseValueList,使用 default(T) 而不是 null。这意味着您不能在列表中使用 0。)您也许可以如果您认为不需要,请取出版本控制 - 大多数游戏可能不需要。

    /// <summary>
    /// Specifying null as value has unspecified results.
    /// CopyTo may contain nulls.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class SparseList<T> : IList<T>
        where T : class
    {
        int version = 0;
        List<T> list = new List<T>();
        Stack<int> freeIndices = new Stack<int>();
    
        public int Capacity { get { return list.Capacity; } set { list.Capacity = value; } }
    
        public void Compact()
        {
            var sortedIndices = freeIndices.ToList();
    
            foreach (var i in sortedIndices.OrderBy(x => x).Reverse())
            {
                list.RemoveAt(i);
            }
            freeIndices.Clear();
            list.Capacity = list.Count;
            version++; // breaks open enumerators
        }
    
        public int IndexOf(T item)
        {
            return list.IndexOf(item);
        }
    
        /// <summary>
        /// Slow (forces a compact), not recommended
        /// </summary>
        /// <param name="index"></param>
        /// <param name="item"></param>
        public void Insert(int index, T item)
        {
            // One idea: remove index from freeIndices if it's in there.  Stack doesn't support this though.
            Compact(); // breaks the freeIndices list, so apply it before insert
            list.Insert(index, item);
            version++; // breaks open enumerators
        }
    
        public void RemoveAt(int index)
        {
            if (index == Count - 1) { list.RemoveAt(index); }
            else { list[index] = null; freeIndices.Push(index); }
            //version++; // Don't increment version for removals
        }
    
        public T this[int index]
        {
            get
            {
                return list[index];
            }
            set
            {
                if (value == null) throw new ArgumentNullException();
                list[index] = value;
            }
        }
    
        public void Add(T item)
        {
            if (item == null) throw new ArgumentNullException();
    
            if (freeIndices.Count == 0) { list.Add(item); return; }
    
            list[freeIndices.Pop()] = item;
            //version++; // Don't increment version for additions?  It could result in missing the new value, but shouldn't break open enumerators
        }
    
        public void Clear()
        {
            list.Clear();
            freeIndices.Clear();
            version++;
        }
    
        public bool Contains(T item)
        {
            if (item == null) return false;
            return list.Contains(item);
        }
    
        /// <summary>
        /// Result may contain nulls
        /// </summary>
        /// <param name="array"></param>
        /// <param name="arrayIndex"></param>
        public void CopyTo(T[] array, int arrayIndex)
        {
            list.CopyTo(array, arrayIndex);
        }
        //public void CopyNonNullTo(T[] array, int arrayIndex)
        //{
        //}
    
        /// <summary>
        /// Use this for iterating via for loop.
        /// </summary>
        public int Count  { get { return list.Count; } }
    
        /// <summary>
        /// Don't use this for for loops!  Use Count.
        /// </summary>
        public int NonNullCount
        {
            get { return list.Count - freeIndices.Count; }
        }
    
        public bool IsReadOnly
        {
            get { return false; }
        }
    
        public bool Remove(T item)
        {
            int i = list.IndexOf(item);
            if (i < 0) return false;
    
            if (i == list.Count - 1)
            {
                // Could throw .  Could add check in 
                list.RemoveAt(i);
            }
            else
            {
                list[i] = null;
                freeIndices.Push(i);
            }
            //version++;  // Don't increment version for removals
            return true;
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return new SparseListEnumerator(this);
        }
    
        private class SparseListEnumerator : IEnumerator<T>, IRemovingEnumerator
        {
            SparseList<T> list;
            int version;
            int index = -1;
    
            public SparseListEnumerator(SparseList<T> list)
            {
                this.list = list;
                this.version = list.version;
    
                //while (Current == null && MoveNext()) ;
            }
    
            public T Current
            {
                get {
                    if (index >= list.Count) return null; // Supports removing last items of collection without throwing on Enumerator access
                    return list[index]; 
                }
            }
    
            public void Dispose()
            {
                list = null;
            }
    
            object IEnumerator.Current
            {
                get { return Current; }
            }
    
            public bool MoveNext()
            {
                do
                {
                    if (version != list.version) { throw new InvalidOperationException("Collection modified"); }
                    index++;
                    return index < list.Count;
                } while (Current == null);
            }
    
            public void Reset()
            {
                index = -1;
                version = list.version;
            }
    
            /// <summary>
            /// Accessing Current after RemoveCurrent may throw a NullReferenceException or return null.
            /// </summary>
            public void RemoveCurrent()
            {
                list.RemoveAt(index);
            }
        }
    
        private class SparseListCleaningEnumerator : IEnumerator<T>, IRemovingEnumerator
        {
            SparseList<T> list;
            int version;
            int index = -1;
    
            public SparseListCleaningEnumerator(SparseList<T> list)
            {
                this.list = list;
                this.version = list.version;
    
                //while (Current == null && MoveNext()) ;
            }
    
            public T Current
            {
                get
                {
                    if (index >= list.Count) return null; // Supports removing last items of collection without throwing on Enumerator access
                    return list[index];
                }
            }
    
            public void Dispose()
            {
                list = null;
            }
    
            object IEnumerator.Current
            {
                get { return Current; }
            }
    
            public bool MoveNext()
            {
                do
                {
                    if (version != list.version) { throw new InvalidOperationException("Collection modified"); }
                    if (index > 0
                        && Current != null // only works for values that are set, otherwise the index is buried in the free index stack somewhere
                        )
                    {
                        int freeIndex = list.freeIndices.Peek();
                        if (freeIndex < index)
                        {
                            list.freeIndices.Pop();
                            list[freeIndex] = list[index];
                            list.RemoveAt(index);
                        }
                    }
                    index++;
                    return index < list.Count;
                } while (Current == null);
            }
    
            public void Reset()
            {
                index = -1;
                version = list.version;
            }
    
            /// <summary>
            /// Accessing Current after RemoveCurrent may throw a NullReferenceException or return null.
            /// </summary>
            public void RemoveCurrent()
            {
                list.RemoveAt(index);
            }
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    
    /// <summary>
    /// Like SparseList but Supports value types, using default(T) in place of null.
    /// This means values of default(T) are not permitted as values in the collection.
    /// CopyTo may contain default(T).
    /// TODO: Use EqualityComparer&lt;T&gt;.Default instead of default(T).Equals()
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class SparseValueList<T> : IList<T>
    {
        int version = 0;
        List<T> list = new List<T>();
        Stack<int> freeIndices = new Stack<int>();
    
        public int Capacity { get { return list.Capacity; } set { list.Capacity = value; } }
    
        public void Compact()
        {
            var sortedIndices = freeIndices.ToList();
    
            foreach (var i in sortedIndices.OrderBy(x => x).Reverse())
            {
                list.RemoveAt(i);
            }
            freeIndices.Clear();
            list.Capacity = list.Count;
            version++; // breaks open enumerators
        }
    
        public int IndexOf(T item)
        {
            return list.IndexOf(item);
        }
    
        /// <summary>
        /// Slow (forces a compact), not recommended
        /// </summary>
        /// <param name="index"></param>
        /// <param name="item"></param>
        public void Insert(int index, T item)
        {
            // One idea: remove index from freeIndices if it's in there.  Stack doesn't support this though.
            Compact(); // breaks the freeIndices list, so apply it before insert
            list.Insert(index, item);
            version++; // breaks open enumerators
        }
    
        public void RemoveAt(int index)
        {
            if (index == Count - 1) { list.RemoveAt(index); }
            else { list[index] = default(T); freeIndices.Push(index); }
            //version++; // Don't increment version for removals
        }
    
        public T this[int index]
        {
            get
            {
                return list[index];
            }
            set
            {
                if (default(T).Equals(value)) throw new ArgumentNullException();
                list[index] = value;
            }
        }
    
        public void Add(T item)
        {
            if (default(T).Equals(item)) throw new ArgumentNullException();
    
            if (freeIndices.Count == 0) { list.Add(item); return; }
    
            list[freeIndices.Pop()] = item;
            //version++; // Don't increment version for additions?  It could result in missing the new value, but shouldn't break open enumerators
        }
    
        public void Clear()
        {
            list.Clear();
            freeIndices.Clear();
            version++;
        }
    
        public bool Contains(T item)
        {
            if (default(T).Equals(item)) return false;
            return list.Contains(item);
        }
    
        /// <summary>
        /// Result may contain default(T)'s
        /// </summary>
        /// <param name="array"></param>
        /// <param name="arrayIndex"></param>
        public void CopyTo(T[] array, int arrayIndex)
        {
            list.CopyTo(array, arrayIndex);
        }
        //public void CopyNonNullTo(T[] array, int arrayIndex)
        //{
        //}
    
        /// <summary>
        /// Use this for iterating via for loop.
        /// </summary>
        public int Count { get { return list.Count; } }
    
        /// <summary>
        /// Don't use this for for loops!  Use Count.
        /// </summary>
        public int NonNullCount
        {
            get { return list.Count - freeIndices.Count; }
        }
    
        public bool IsReadOnly
        {
            get { return false; }
        }
    
        public bool Remove(T item)
        {
            int i = list.IndexOf(item);
            if (i < 0) return false;
    
            if (i == list.Count - 1)
            {
                // Could throw .  Could add check in 
                list.RemoveAt(i);
            }
            else
            {
                list[i] = default(T);
                freeIndices.Push(i);
            }
            //version++;  // Don't increment version for removals
            return true;
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return new SparseValueListEnumerator(this);
        }
    
        private class SparseValueListEnumerator : IEnumerator<T>, IRemovingEnumerator
        {
            SparseValueList<T> list;
            int version;
            int index = -1;
    
            public SparseValueListEnumerator(SparseValueList<T> list)
            {
                this.list = list;
                this.version = list.version;
    
                //while (Current == default(T) && MoveNext()) ;
            }
    
            public T Current
            {
                get
                {
                    if (index >= list.Count) return default(T); // Supports removing last items of collection without throwing on Enumerator access
                    return list[index];
                }
            }
    
            public void Dispose()
            {
                list = null;
            }
    
            object IEnumerator.Current
            {
                get { return Current; }
            }
    
            public bool MoveNext()
            {
                do
                {
                    if (version != list.version) { throw new InvalidOperationException("Collection modified"); }
                    index++;
                    return index < list.Count;
                } while (default(T).Equals(Current));
            }
    
            public void Reset()
            {
                index = -1;
                version = list.version;
            }
    
            /// <summary>
            /// Accessing Current after RemoveCurrent may throw a NullReferenceException or return default(T).
            /// </summary>
            public void RemoveCurrent()
            {
                list.RemoveAt(index);
            }
        }
    
        private class SparseValueListCleaningEnumerator : IEnumerator<T>, IRemovingEnumerator
        {
            SparseValueList<T> list;
            int version;
            int index = -1;
    
            public SparseValueListCleaningEnumerator(SparseValueList<T> list)
            {
                this.list = list;
                this.version = list.version;
    
                while (default(T).Equals(Current) && MoveNext()) ;
            }
    
            public T Current
            {
                get
                {
                    if (index >= list.Count) return default(T); // Supports removing last items of collection without throwing on Enumerator access
                    return list[index];
                }
            }
    
            public void Dispose()
            {
                list = null;
            }
    
            object IEnumerator.Current
            {
                get { return Current; }
            }
    
            public bool MoveNext()
            {
                do
                {
                    if (version != list.version) { throw new InvalidOperationException("Collection modified"); }
                    if (index > 0 
                        && (!default(T).Equals(Current)) // only works for values that are set, otherwise the index might be buried in the stack somewhere
                        )
                    {
                        int freeIndex = list.freeIndices.Peek();
                        if (freeIndex < index)
                        {
                            list.freeIndices.Pop();
                            list[freeIndex] = list[index];
                            list.RemoveAt(index);
                        }
                    }
                    index++;
                    return index < list.Count;
                } while (default(T).Equals(Current));
            }
    
            public void Reset()
            {
                index = -1;
                version = list.version;
            }
    
            /// <summary>
            /// Accessing Current after RemoveCurrent may throw a NullReferenceException or return default(T).
            /// </summary>
            public void RemoveCurrent()
            {
                list.RemoveAt(index);
            }
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2012-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-28
      • 2020-07-27
      相关资源
      最近更新 更多