在 C# 中是不是有更快的方法来过滤基于 DateTime 的字典?
Posted
技术标签:
【中文标题】在 C# 中是不是有更快的方法来过滤基于 DateTime 的字典?【英文标题】:Is there a faster method to filter a DateTime based Dictionary in C#?在 C# 中是否有更快的方法来过滤基于 DateTime 的字典? 【发布时间】:2021-03-07 07:03:27 【问题描述】:非常简单,我将 TimeSeries 存储为带有 IDictionary<Datetime, double?>
签名的 DateTime 字典。这将包含一个月的 10 分钟分辨率数据,因此最多 4,464 个条目。
为了处理这个时间序列的部分,我们需要在 start
和 end
DateTime
之间提取一个部分。
这样做的一种天真的方法是获取我们感兴趣的范围的字典键的子集:
var reducedKeys = timeSeries.Keys.Where(k => k >= start && k <= end).ToList();
然后从大timeSeries
中提取相关部分
var reducedTimeSeries = timeSeries.Where(kvp => reducedKeys .Contains(kvp.Key)).ToDictionary(w => w.Key, w => w.Value);
感觉这不是最佳解决方案;对更快的提取策略有什么建议吗?
为了清楚起见,时间戳的顺序在此阶段基本上是无关紧要的,因为更高级别的计算发生在多个时间序列中,而不是在同一个序列中。提取后有一个扁平线过滤器要运行;但这可以通过迭代时间序列提取中的键的排序副本来运行,因为在从较长的源序列中提取后,我们通常会有 12-24 个样本序列。
【问题讨论】:
“更好”和“最佳”都是广义词,包含许多不同的因素。请告诉我们您最感兴趣的指标。“所有指标”是不可接受的:)。另请阅读Eric Lippert's blog post on performance。 感谢@HereticMonkey,这是一本好书 :-) 在这种情况下,我们正在考虑的绝对是速度,因为该操作是在一个不适合基于设置的处理的大型循环操作中。 如果您不介意向您的应用添加第三方依赖项,您可以考虑使用C5 库。它有一个TreeDictionary
集合,其中包含方法 RangeFromTo
,该方法有效地返回集合内的一系列键。
如果偏移量总是来自同一个日期,那么你可以只使用一个带有开始日期的数组,并且每个后续元素都是10分钟的偏移量
【参考方案1】:
Dictionary<TKey, TValue>
根据TKey
值的哈希值将其条目存储在存储桶中。简而言之,您的时间序列条目未按顺序存储。这使得使用日期时间范围非常低效,因为您需要枚举所有项目以获取适当的项目。
您可以考虑使用SortedDictionary<TKey, TValue>
,因为它结合了TKey
值的散列和排序。请参阅此回复:https://***.com/a/9053294/1323798。
如果您关心性能,我个人会寻找专门设计用于时间序列数据的数据结构。但与往常一样,这完全取决于您打算如何处理时间序列。
【讨论】:
我一直对使用 SortedDictionary 有点谨慎,但在这种情况下,它可能是一个不错的选择,因为我们在从数据库中提取数据时构建数据集,然后广泛使用数据。值得一试:-)【参考方案2】:不知道这是否是最佳解决方案,但肯定更短且更容易理解
var reducedTimeSeries = timeSeries.Where(x => x.Key >= start && x.Key <= end).Select(x => x)
【讨论】:
您可以删除Select()
调用。【参考方案3】:
如果 SortedDictionary 不适合您,那么您可以创建自己的自定义解决方案。
例如,如果偏移量是恒定的,您可以将数据存储在数组中并按偏移量进行搜索。
就我而言,自定义解决方案的速度提高了 3 倍
//(1000 iterations)
// DateTimeArray:00:00:00.1629569
// SortedDictionary:00:00:00.5831970
public sealed class DateTimeArray<TValue>
private readonly DateTime _startDate;
private readonly int _step;
private readonly int _size;
private TValue[] _values;
public DateTimeArray(DateTime startDate, int step = 10, int size = 4464)
if (step <= 0)
throw new InvalidDataException("step can not be less than 1");
_startDate = startDate;
_step = step;
_size = size;
_values = new TValue[size];
public DateTimeArray(DateTime startDate, DateTime endDate, int step = 10)
: this(startDate, step, (int) ((endDate - startDate).TotalMinutes / step))
public void Add(DateTime date, TValue value)
var offset = (int)((date - _startDate).TotalMinutes);
var current = offset / _step;
if (_size <= current)
throw new IndexOutOfRangeException($"current>=_size");
_values[current] = value;
public TValue[] Between(DateTime from, DateTime? to = null)
var offsetFrom = (int)((from - _startDate).TotalMinutes) / _step;
var offsetTo = _size;
if (to.HasValue)
offsetTo = (int) ((to.Value- _startDate).TotalMinutes) / _step;
if (offsetFrom >= offsetTo)
throw new IndexOutOfRangeException($"offsetFrom>=offsetTo");
return _values.Skip(offsetFrom).Take(offsetTo - offsetFrom ).ToArray();
【讨论】:
【参考方案4】:Dictionary<DateTime, double?>
实现接口IEnumerable<KeyValuePair<DateTime, double?>>
。因此,如果您希望字典中的所有值都介于两个 DateTimes 之间,请使用此界面选择要放入新字典中的键值对。
DateTime startTime = ...
Datetime endTime = ...
Dictionary<DateTime, double?> dictionary = ...
var itemsToPutInNewDictionary = dictionary
.Where(keyValuePair => startTime <= keyValuePair.Key
&& endTime >= keyValuePair.Key);
// note: the query is not executed yet!
var limitedDictionary = new Dictionary<DateTime, double?>(itemsToPutInNewDictionary);
或者您可以在 LINQ 末尾使用 ToDictionary:
.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value);
您必须查看字典是如何构建的,但我认为后一种解决方案较慢。
【讨论】:
【参考方案5】:您可以将Dictionary
的内容复制到一个排序数组中,然后使用Array.BinarySearch
方法以O(log n) 的效率在数组中搜索日期。
下面是一个扩展方法RangeFromTo
用于字典的排序副本。它返回两个键之间的内容范围:
public static IEnumerable<KeyValuePair<TKey, TValue>> RangeFromTo<TKey, TValue>(
this KeyValuePair<TKey, TValue>[] sortedArray,
TKey from, TKey to)
var keyComparer = Comparer<TKey>.Default;
var pairComparer = Comparer<KeyValuePair<TKey, TValue>>.Create(
(x, y) => keyComparer.Compare(x.Key, y.Key));
int fromIndex = Array.BinarySearch(sortedArray,
new KeyValuePair<TKey, TValue>(from, default), pairComparer);
int toIndex = Array.BinarySearch(sortedArray,
new KeyValuePair<TKey, TValue>(to, default), pairComparer);
if (fromIndex < 0) fromIndex = ~fromIndex;
if (toIndex < 0) toIndex = Math.Min(~toIndex, sortedArray.Length - 1);
for (int i = fromIndex; i <= toIndex; i++)
yield return sortedArray[i];
使用示例:
var dictionary = new Dictionary<DateTime, double>();
/* Code that fills the dictionary omitted */
var sortedArray = dictionary.OrderBy(e => e.Key).ToArray();
var selection = sortedArray.RangeFromTo(
new DateTime(2020, 1, 1), new DateTime(2020, 12, 31));
foreach (var pair in selection)
DateTime date = pair.Key;
double value = pair.Value;
Console.WriteLine($"date:yyyy/MM/dd => value");
有一个问题:每次更新字典时都必须重新创建排序数组。这两者必须始终保持同步。
警告:您应该对每个副本进行足够的搜索,以证明其成本合理。理想情况下,您应该能够使用单个副本进行多对多搜索。在极其不利的情况下,您使用每个副本进行单次搜索,其性能将明显比简单的线性搜索差。
【讨论】:
排序数据是 O(nlog(n))。进行线性搜索是 O(n)。你刚刚让它的表现明显*差,而不是更好。 @Servy 是的,如果每次搜索都必须对字典进行排序,那肯定会更糟。希望 OP 有一本不经常更新但需要经常搜索的字典。以上是关于在 C# 中是不是有更快的方法来过滤基于 DateTime 的字典?的主要内容,如果未能解决你的问题,请参考以下文章
在 SAS 中,如果空变量不存在,是不是有更快的方法来创建它?