非常大的集合的效率;迭代和排序

Efficiency of very large collections; iteration and sort

我有一个 csv 解析器,它读取 15+ 百万行(有很多重复项),一旦解析为结构,就需要添加到集合中。每个结构都有属性 Key (int)、A(datetime) 和 B(int)(以及此处不相关的其他属性)。

要求 A: 集合需要通过 Key 强制唯一性。

要求 B: 在后面的步骤中,我需要按属性 A(timestamp) 然后 B(int) 对集合进行排序。

约束: 结构最终需要按顺序遍历,一个一个地引用邻居(LinkedList 在这里提供了最干净的解决方案);此操作的要点是对集合进行分区。请假设这是最早可以发生分区的(即,它不能在解析阶段进行分区)。

我发现 SortedSet 对于要求 A 工作得很好,而且它的性能也相当好,即使 O(log n) 插入比 HashSet<T> 的 O(1 ), 尽管我不关心键的排序。当集合变大时,HashSet<T> 会陷入困境,这显然是一个已知问题,而 SortedSet<T> 不会遇到这个问题。

问题: 当我到达要求 B 的步骤时,对集合进行排序(SortedSet<T> 作为 IEnumerable<T> 传递给方法)需要令人望而却步的时间(20 多分钟的磨合,全部在内存中,不使用页面文件)。

问题:哪个集合最适合解决这个问题?一个想法是使用两个集合:一个强制唯一性(如键的 HashSet<int>SortedSet<int>),第二个 SortedSet<T> 在解析阶段处理排序(即,到目前为止尽可能上游)。但是应用程序已经是内存密集型的,需要页面文件的性能损失是令人望而却步的。
对于按一个特征强制唯一性但按其他不相关特征排序的单个集合,这给我留下了什么选择? SortedSet<T> 使用了 IComparer<T>(但不是同时使用 IComparer<T>IEquitable<T>),所以如果它依赖 CompareTo 来强制唯一性,那么它似乎不符合我的要求。子类化 SortedSet 是可行的方法吗?

编辑: 排序代码:

SortedSet<Dto> parsedSet = {stuff};
var sortedLinkedStructs = new LinkedList<Dto>(parsedSet.OrderBy(t => t.Timestamp).ThenBy(i => i.SomeInt));

结构:

public readonly struct Dto: IEquatable<Dto>, IComparer<Dto>, IComparable<Dto>
{
     public readonly datetime Timestamp;
     public readonly int SomeInt;
     public readonly int Key;

     ctor(ts, int, key){assigned}

     public bool Equals(Dtoother) => this.Key == other.Key;
     public override int GetHashCode() => this.Key.GetHashCode();
     public int Compare(Dto x, Dto y) =>  x.Key.CompareTo(y.Key);
     public int CompareTo(Dto other) => this.Key.CompareTo(other.Key);
}

这可能不是一个直接的答案,但是:这是我在类似规模的类似系统中成功使用的一种方式。这是针对在 Stack Overflow 上驱动问题列表的 "tag engine";本质上,我有一个:

struct Question {
    // basic members - score, dates, id, etc - no text
}

基本上一个超大的Question[](实际上我在非托管内存中使用Question*,但那是因为我需要能够与一些 GPU 代码出于不相关的原因)。填充数据只是取出 Question[] 中的连续行。此数据永远不会排序 - 它作为源数据单独保留 - 只需附加(新键)或覆盖(相同键); 在最坏的情况下如果我们达到最大容量,我们可能需要重新分配数据并将数据块复制到新数组。

现在,我 单独 保留一个 int[](实际上 int* 的原因与之前相同,但... meh),其中 int[] 中的每个值都是 Question[]actual 数据的 index。所以最初它可能是 0, 1, 2, 3, 4, 5, ...(虽然我预先过滤了它,所以它只包含我想保留的行 - 删除 "deleted" 等)。

使用或者修饰符并行快速排序(参见) or a modified "introspective sort" (like here)——所以在排序结束时,我可能有0, 3, 1, 5, ....

现在:为了遍历数据,我只是遍历 int[],并将其用作对 [=14= 中的 实际 数据的查找].这最大限度地减少了排序期间的数据移动量,并允许我非常有效地保持多个单独的排序(可能使用不同的预过滤器)。对 15M 数据进行排序只需要几毫秒(每分钟左右发生一次,以便将新问题引入 Stack Overflow,或记录对现有问题的更改)。

为了尽可能快地进行排序,我尝试编写我的排序代码,以便可以用 单个 整数值表示复合排序,从而实现非常有效的排序(内省排序可用)。例如,下面是 "last activity date, then question id" 排序的代码:

public override bool SupportsNaturallySortableUInt64 => true;
public override unsafe ulong GetNaturallySortableUInt64(Question* question)
{
    // compose the data (MSB) and ID (LSB)
    var val = Promote(question->LastActivityDate) << 32
        | Promote(question->Id);
    return ~val; // the same as ulong.MaxValue - val (which reverses order) but much cheaper
}

这通过将 LastActivityDate 视为 32 位整数,左移 32 位并将其与 Id 组合为 32 位整数来工作,这意味着我们可以比较日期和 id 在一次操作中。

或 "score, then answer score, then id":

public override unsafe ulong GetNaturallySortableUInt64(Question* question)
{
    // compose the data
    var val = Promote(question->Score) << 48
        | Promote(question->AnswerScore) << 32
        | Promote(question->Id);
    return ~val; // the same as ulong.MaxValue - val (which reverses order) but much cheaper
}

请注意,每个元素仅调用一次 GetNaturallySortableUInt64 - 进入相同大小的 ulong[](是的,实际上是 ulong*)的工作区域,因此最初两个工作区类似于:

int[]    ulong[]
0        34243478238974
1        12319388173
2        2349245938453
...      ...

现在我可以通过只查看一个 int[] 和一个 ulong[] 来完成整个排序,这样 ulong[] 向量就会按排序顺序结束,而 int[] 包含要查看的项目的索引。