【问题标题】:Most efficient structure for quick access to a random element + the last element on one criteria快速访问随机元素 + 一个标准的最后一个元素的最有效结构
【发布时间】:2015-10-21 12:22:37
【问题描述】:

我们有一个函数通知我们收到了一个特定时间戳的项目。

这样做的目的是等待一个特定的时间戳,我们等待我们收到我们期望的每个项目,然后在我们与所有项目“同步”后进一步推送通知。

目前,我们有一个 Dictionary<DateTime, TimeSlot> 来存储非同步 TimeSlot(TimeSlot = 我们收到的特定时间戳的所有项目的列表)。

//Let's assume that this method is not called concurrently, and only once per "MyItem"
public void HandleItemReceived(DateTime timestamp, MyItem item){
    TimeSlot slot;
    //_pendingTimeSlot is a Dictionary<DateTime,TimeSlot>
    if(!_pendingTimeSlot.TryGetValue(timestamp, out slot)){
        slot = new TimeSlot(timestamp);
        _pendingTimeSlot.Add(timestamp,slot );

        //Sometimes we don't receive all the items for one timestamps, which may leads to some ghost-incomplete TimeSlot
        if(_pendingTimeSlot.Count>_capacity){
            TimeSlot oldestTimeSlot = _pendingTimeSlot.OrderBy(t=>t.Key).Select(t=>t.Value).First();
            _pendingTimeSlot.Remove(oldestTimeSlot.TimeStamp);
            //Additional work here to log/handle this case
        }
    }
    slot.HandleItemReceived(item);
    if(slot.IsComplete){
        PushTimeSlotSyncronized(slot);
        _pendingTimeSlot.Remove(slot.TimeStamp);
    }
}

对于不同的项目组,我们有多个并行的“同步器”实例。

它工作正常,除了当系统负载很重时,我们有更多不完整的 TimeSlot,并且应用程序使用了更多的 CPU。探查器似乎表明 LINQ 查询的 Compare 花费了很多时间(大部分时间)。所以我试图找到一些结构来保存这些引用(替换字典)

以下是一些指标:

  • 我们有几个(可变,但在 10 到 20 之间)这个同步器的实例
  • 同步器当前最大容量(_capacity)为500项
  • 两个不同时间戳之间的最短间隔是 100 毫秒(因此每个同步器每秒有 10 个新的字典条目)(大多数情况下更多的是 1 项/秒)
  • 对于每个时间戳,我们预计会收到 300-500 个项目。

所以我们将这样做,对于一个同步器,每秒(最坏情况):

  • 1 添加
  • 500 获取
  • 3-5 次排序

我最好的举动是什么?我想到了SortedDictionary 但我没有找到任何文档告诉我如何根据密钥获取第一个元素。

【问题讨论】:

  • 我不认为这是基于意见的——算法复杂度是一个可衡量的数量。
  • 稍微切线:rawgit.com/rehansaeed/…
  • 最好的解决方案是使用 List 而不是 Dictionary。我会按时间顺序将项目添加到列表中,因此无需执行排序。我会检查列表中的最后一项以及要添加的项目,以确保新项目晚于最后一项。如果没有,则从列表末尾向后工作,直到将新项目按顺序放入列表中。
  • _pendingTimeSlot 真的是字典吗?我问是因为没有TryGet 方法,OrderBy(..).First() 的结果应该是'KeyValuePair',而不是TimeSlot
  • @IvanStoev 是的,它是一本字典,但我必须对这段代码进行大量简化以获得可读的形式,所以它不是我真正的代码。但是你是对的,调用的是TryGetValue方法,order by应该选择Value。我会更新的

标签: c# linq dictionary data-structures


【解决方案1】:

您可以尝试的第一件事是消除OrderBy - 您只需要最小键,无需排序即可:

if (_pendingTimeSlot.Count > _capacity) {
    // No Enumerable.Min(DateTime), so doing it manually
    var oldestTimeStamp = DateTime.MaxValue;
    foreach (var key in _pendingTimeSlot.Keys)
        if (oldestTimeStamp > key) oldestTimestamp = key;
    _pendingTimeSlot.Remove(oldestTimeStamp);
    //Additional work here to log/handle this case
}

SortedDictionary 呢,它肯定是一个选项,虽然它会消耗更多的内存。由于它已排序,您可以简单地使用 sortedDictionary.First() 来获取具有最小键的键值对(因此在您的情况下是最旧的元素)。

更新:这是一种混合方法,使用字典进行快速查找,并使用有序双链表用于其他场景。

class MyItem
{
    // ...
}
class TimeSlot
{
    public readonly DateTime TimeStamp;
    public TimeSlot(DateTime timeStamp)
    {
        TimeStamp = timeStamp;
        // ...
    }
    public bool IsComplete = false;
    public void HandleItemReceived(MyItem item)
    {
        // ...
    }
    // Dedicated members
    public TimeSlot PrevPending, NextPending;
}
class Synhronizer
{
    const int _capacity = 500;
    Dictionary<DateTime, TimeSlot> pendingSlotMap = new Dictionary<DateTime, TimeSlot>(_capacity + 1);
    TimeSlot firstPending, lastPending;

    //Let's assume that this method is not called concurrently, and only once per "MyItem"
    public void HandleItemReceived(DateTime timeStamp, MyItem item)
    {
        TimeSlot slot;
        if (!pendingSlotMap.TryGetValue(timeStamp, out slot))
        {
            slot = new TimeSlot(timeStamp);
            Add(slot);
            //Sometimes we don't receive all the items for one timestamps, which may leads to some ghost-incomplete TimeSlot
            if (pendingSlotMap.Count > _capacity)
            {
                // Remove the oldest, which in this case is the first
                var oldestSlot = firstPending;
                Remove(oldestSlot);
                //Additional work here to log/handle this case
            }
        }
        slot.HandleItemReceived(item);
        if (slot.IsComplete)
        {
            PushTimeSlotSyncronized(slot);
            Remove(slot);
        }
    }

    void Add(TimeSlot slot)
    {
        pendingSlotMap.Add(slot.TimeStamp, slot);
        // Starting from the end, search for a first slot having TimeStamp < slot.TimeStamp
        // If the TimeStamps almost come in order, this is O(1) op.
        var after = lastPending;
        while (after != null && after.TimeStamp > slot.TimeStamp)
            after = after.PrevPending;
        // Insert the new slot after the found one (if any).
        if (after != null)
        {
            slot.PrevPending = after;
            slot.NextPending = after.NextPending;
            after.NextPending = slot;
            if (slot.NextPending == null) lastPending = slot;
        }
        else
        {
            if (firstPending == null)
                firstPending = lastPending = slot;
            else
            {
                slot.NextPending = firstPending;
                firstPending.PrevPending = slot;
                firstPending = slot;
            }
        }
    }

    void Remove(TimeSlot slot)
    {
        pendingSlotMap.Remove(slot.TimeStamp);
        if (slot.NextPending != null)
            slot.NextPending.PrevPending = slot.PrevPending;
        else
            lastPending = slot.PrevPending;
        if (slot.PrevPending != null)
            slot.PrevPending.NextPending = slot.NextPending;
        else
            firstPending = slot;
        slot.PrevPending = slot.NextPending = null;
    }

    void PushTimeSlotSyncronized(TimeSlot slot)
    {
        // ...
    }
}

一些额外的用法:

从最旧到最新迭代:

for (var slot = firstPending; slot != null; slot = slot.NextPending)
{
    // do something
}

从最旧到最新迭代并根据条件删除项目:

for (TimeSlot slot = firstPending, nextSlot; slot != null; slot = nextSlot)
{
    nextSlot = slot.NextPending;
    if (ShouldRemove(slot))
        Remove(slot);
}

同样适用于反向场景,但使用 lastPendingPrevPending 成员代替。

【讨论】:

  • 我真的很喜欢这个想法,它适用于我解释的情况,但我们还有另一种情况:有时我们必须通知时间段按顺序完成。我的意思是,在通知 T2 完成之前,我们必须等待 T1 完成,所以也许当 T1 完成时,我们也可以通知 T2/3/...5 也完成。使用您的方式将意味着我们将迭代 5(在此示例中)时间到我们的字典。你还认为我们会从你的实施中受益吗(反对SortedDictionary)?
  • 我也想知道:由于新项目通常必须放在最后(因为更近),所以插入数据更快吗?
  • @J4N 它基于问题中的用例。如您所知,没有完美的数据结构适用于所有内容,您的额外案例显着改变了需求。所以你肯定需要某种排序的容器。 SortedDictionary 是选项之一。如果您几乎总是在列表末尾添加并且几乎总是从列表的开头删除,则某些自定义数据结构可能会执行得更好。你还有其他用例吗?也可以在TimeSlot 类中添加一些专用成员吗?
  • 不,我最初没有提到的唯一情况是我们尝试删除 X 最旧的数据时的情况(抱歉,我认为这不会有很大影响)。 TimeSlot 绝对可以根据需要进行修改。你心目中的敬业会员是什么样的?
  • @J4N 我正在考虑添加 2 个 TimeSlot 字段以用于双链挂起列表。
【解决方案2】:

这是一个简单的示例。列表中的插入方法消除了交换元素。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Data> inputs = new List<Data>() {
                new Data() { date = DateTime.Parse("10/22/15 6:00AM"), data = "abc"},
                new Data() { date = DateTime.Parse("10/22/15 4:00AM"), data = "def"},
                new Data() { date = DateTime.Parse("10/22/15 6:30AM"), data = "ghi"},
                new Data() { date = DateTime.Parse("10/22/15 12:00AM"), data = "jkl"},
                new Data() { date = DateTime.Parse("10/22/15 3:00AM"), data = "mno"},
                new Data() { date = DateTime.Parse("10/22/15 2:00AM"), data = "pqr"},
            };

            Data data = new Data();
            foreach (Data input in inputs)
            {
                data.Add(input);
            }
        }
    }
    public class Data
    {
        public static List<Data> sortedData = new List<Data>();
        public DateTime date { get; set; }
        public string data { get; set;}


        public void Add(Data newData)
        {
            if(sortedData.Count == 0)
            {
                sortedData.Add(newData);
            }
            else
            {
               Boolean added = false;
               for(int index = sortedData.Count - 1; index >= 0; index--)
               {
                   if(newData.date > sortedData[index].date)
                   {
                       sortedData.Insert(index + 1, newData);
                       added = true;
                       break;
                   }
               }
               if (added == false)
               {
                   sortedData.Insert(0, newData);
               }
            }
        }
    }

}​

【讨论】:

  • 但实际上在我解释的情况下,每个时间戳都会有很多条目,所以我必须能够向每个时间戳添加多个数据(在我的示例中,包含的类所有的项目都称为TimeSlot。这意味着我必须为给定的时间戳检索很多时间(大部分时间)特定的TimeSlot,然后将我的数据添加到其中。我的印象是必须为每个时间戳检索 500 倍的时隙会很慢。
  • 那么字符串数据就变成了List数据。您将使用什么标准来检索数据。我认为在你的情况下插入是实时的,但提取不需要实时完成?最重要的因素是能够实时存储数据。所以你需要一个高效的存储算法。如果您有开销,那么有序地存储数据会很好。您可能没有开销,因此按时间顺序存储可能是您可以达到的最佳效果。在确定最佳方法之前,您需要检查性能。
  • 是的,但是请检查我的代码,以便能够为 List&lt;String&gt; 添加一些值,我必须能够先检索此列表以将项目添加到其中。每个时间段我要添加约 500 个项目
  • 如果您有足够的处理时间,可以将 List 制作成字典。你必须做出那个决定。对于实时处理,始终需要进行测试,有时需要在最终确定应用程序之前进行改进。
  • 对不起,我不明白。我们不会在同一次调用中收到给定时间戳的所有数据。每次我们收到一个数据,我们都要检查是否已经有这个时间的TimeSlot,如果是的话,将数据添加到现有的timeslot中。否则,将不需要同步
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
  • 2021-03-04
  • 2011-11-07
  • 1970-01-01
  • 1970-01-01
  • 2015-09-25
相关资源
最近更新 更多