【问题标题】:How do I keep a list of only the last n objects?如何保留仅包含最后 n 个对象的列表?
【发布时间】:2011-06-17 22:37:45
【问题描述】:

我想对特定方法进行一些性能测量,但我想平均完成所需的时间。 (这是一个 C# Winforms 应用程序,但这个问题很可能适用于其他框架。)

我有一个秒表,我在方法开始时重置并在结束时停止。 我想将最后 10 个值存储在列表或数组中。添加的每个新值都应将最旧的值排除在列表之外。

我会定期调用另一个方法来平均所有存储的值。

我认为这个构造是一个循环缓冲区是否正确?

如何创建具有最佳性能的缓冲区?现在我有以下内容:

List<long> PerfTimes = new List<long>(10);

// ...

private void DoStuff()
{
    MyStopWatch.Restart();
    // ...
    MyStopWatch.Stop();
    PerfTimes.Add(MyStopWatch.ElapsedMilliseconds);
    if (PerfTimes.Count > 10) PerfTimes.RemoveAt(0);
}

不知何故,这似乎效率低下,但也许不是。

建议?

【问题讨论】:

  • 使用分析器有什么问题?
  • @Brandon,我计划使用平均值向用户显示,作为解析对象花费多长时间的指标。这是图形翻译实用程序的一部分。

标签: c# collections circular-buffer


【解决方案1】:

您可以创建一个自定义集合:

class SlidingBuffer<T> : IEnumerable<T>
{
    private readonly Queue<T> _queue;
    private readonly int _maxCount;

    public SlidingBuffer(int maxCount)
    {
        _maxCount = maxCount;
        _queue = new Queue<T>(maxCount);
    }

    public void Add(T item)
    {
        if (_queue.Count == _maxCount)
            _queue.Dequeue();
        _queue.Enqueue(item);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _queue.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

您当前的解决方案有效,但效率低下,因为删除 List&lt;T&gt; 的第一项成本很高。

【讨论】:

  • 又短又漂亮。
  • 美丽的事物是永远的快乐,很高兴看到如此优雅的代码:)
【解决方案2】:
private int ct = 0;
private long[] times = new long[10];

void DoStuff ()
{
   ...
   times[ct] = MyStopWatch.ElapsedMilliseconds;
   ct = (ct + 1) % times.Length; // Wrap back around to 0 when we reach the end.
}

这是一个简单的圆形结构。 这不需要其他解决方案所具有的链表节点的数组复制或垃圾回收。

【讨论】:

  • 我将不得不在一段时间内实现这一点,在看到您的答案之前我已经实现了一个队列,但这绝对可以避免您提到的一些性能损失。
  • 一个小问题,你将times初始化为零长度,显然这个数字应该改变为缓冲区应该有的任何深度。
  • @JYelton 多年后,我将这个与我的 answer 中的队列实现结合起来
【解决方案3】:

为了获得最佳性能,您可能只使用 long 数组而不是列表。

我们曾经有类似的要求来实现下载时间估算器,我们使用循环缓冲区来存储最后N 秒的速度。

我们对整个时间的下载速度不感兴趣,只是根据最近的活动大致预计需要多长时间,而不是如此最近以至于数字会跳跃在这个地方(例如,如果我们只是使用最后一秒来计算它)。

我们对整个时间范围不感兴趣的原因是,下载可能会在半小时内达到 1M/s,然后在接下来的 10 分钟内切换到 10M/s。尽管您现在的下载速度非常快,但前半小时会严重降低平均速度。

我们创建了一个循环缓冲区,每个单元格保存 1 秒内的下载量。循环缓冲区大小为 300,允许 5 分钟的历史数据,并且每个单元都初始化为零。在您的情况下,您只需要十个单元格。

我们还维护了一个总数(缓冲区中所有条目的总和,因此最初也是零)和计数(显然最初是零)。

每一秒,我们都会计算出自上一秒以来下载了多少数据,然后:

  • 从总数中减去当前单元格。
  • 将当前图形放入该单元格并推进单元格指针。
  • 将当前数字加到总数中。
  • 如果还不是 300,则增加计数。
  • 根据总数/计数更新显示给用户的数字。

基本上,在伪代码中:

def init (sz):
    buffer = new int[sz]
    for i = 0 to sz - 1:
        buffer[i] = 0 
    total = 0
    count = 0
    index = 0
    maxsz = sz

def update (kbps):
    total = total - buffer[index] + kbps   # Adjust sum based on deleted/inserted values.
    buffer[index] = kbps                   # Insert new value.
    index = (index + 1) % maxsz            # Update pointer.
    if count < maxsz:                      # Update count.
        count = count + 1
    return total / count                   # Return average.

这应该很容易适应您自己的要求。总和是“缓存”信息的一个很好的功能,它可以让你的代码更快。我的意思是:如果您需要计算总和或平均值,则只有在数据发生变化时才能计算出来,并使用最少的必要计算。

另一种方法是在请求时将所有十个数字相加,这在将另一个值加载到缓冲区时会比单个减法/加法要慢。

【讨论】:

    【解决方案4】:

    您可能希望改用队列数据结构。你可以使用一个简单的线性列表,但它完全是低效的。可以使用圆形数组,但您必须不断调整它的大小。因此,我建议您使用队列。

    【讨论】:

    • +1 感谢 Queue 的建议,这就是我最终使用的。 @Thomas 提供了一些代码示例,我觉得这很有帮助。
    【解决方案5】:

    我需要在一个数组中保留 5 个最后的分数,我想出了这个简单的解决方案。 希望对大家有所帮助。

    void UpdateScoreRecords(int _latestScore){
            latestScore = _latestScore;
            for (int cnt = 0; cnt < scoreRecords.Length; cnt++) {
                if (cnt == scoreRecords.Length - 1) {
                    scoreRecords [cnt] = latestScore;
                } else {
                    scoreRecords [cnt] = scoreRecords [cnt+1];
                }
            }
        }
    

    【讨论】:

      【解决方案6】:

      我觉得没问题。改用 LinkedList 怎么样?使用 List 时,如果删除第一项,则所有其他项都必须退回一项。使用 LinkedList,您可以以很少的成本在列表中的任何位置添加或删除项目。但是,我不知道这会有多大的不同,因为我们只讨论了十个项目。

      链表的权衡是您不能有效地访问列表的随机元素,因为链表必须基本上“沿着”列表“行走”,传递每个项目,直到它到达您需要的那个.但是对于顺序访问,链表就可以了。

      【讨论】:

        【解决方案7】:

        对于java来说,可能是这样的

        import java.util.Iterator;
        import java.util.LinkedList;
        import java.util.Queue;
        
        public class SlidingBuffer<T> implements Iterable<T>{
            private Queue<T> _queue;
            private int _maxCount;
        
            public SlidingBuffer(int maxCount) {
                _maxCount = maxCount;
                _queue =  new LinkedList<T>();
            }
        
            public void Add(T item) {
                if (_queue.size() == _maxCount)
                    _queue.remove();
                _queue.add(item);
            }
        
            public Queue<T> getQueue() {
                return _queue;
            }
        
            public Iterator<T> iterator() {
                return  _queue.iterator();
            }
        }
        

        可以这样开始

        public class ListT {
        
            public static void main(String[] args) {
                start();
            }
        
            private static void start() {
                SlidingBuffer<String> sb = new SlidingBuffer<>(5);
                sb.Add("Array1");
                sb.Add("Array2");
                sb.Add("Array3");
                sb.Add("Array4");
                sb.Add("Array5");
                sb.Add("Array6");
                sb.Add("Array7");
                sb.Add("Array8");
                sb.Add("Array9");
        
                //Test printout
                for (String s: sb) {
                    System.out.println(s);
                }
            }
        }
        

        结果是

        数组5

        数组6

        数组7

        数组8

        数组9

        【讨论】:

          【解决方案8】:

          在最新答案发布多年后,我在寻找相同的解决方案时偶然发现了这个问题。我以上述答案的组合结束,尤其是以下答案之一:cycling by agent-jusing a queue by Thomas Levesque

          public class SlidingBuffer<T> : IEnumerable<T>
          {
              protected T[] items;
              protected int index = -1;
              protected bool hasCycled = false;
          
              public SlidingBuffer(int windowSize) 
              {
                  items = new T[windowSize];
              }
          
              public void Add(T item)
              {
                  index++;
                  if (index >= items.Length) {
                      hasCycled = true;
                      index %= items.Length;
                  }
          
                  items[index] = item;
              }
          
              public IEnumerator<T> GetEnumerator()
              {
                  if (index == -1)
                      yield break;
          
                  for (int i = index; i > -1; i--)
                  {
                      yield return items[i];
                  }
          
                  if (hasCycled) 
                  {
                      for (int i = items.Length-1; i > index; i--)
                      {
                          yield return items[i];
                      }
                  }
              }
          
              IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
          }
          

          我不得不放弃j-agent 的非常优雅的单行:ct = (ct + 1) % times.Length; 因为我需要检测我们何时回旋(通过hasCycled)才能拥有一个表现良好的枚举器。请注意,枚举器返回的值从 最近的最旧的 值。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2019-07-17
            • 1970-01-01
            • 1970-01-01
            • 2021-12-29
            • 1970-01-01
            • 2018-10-29
            • 1970-01-01
            相关资源
            最近更新 更多