【问题标题】:How can I efficiently remove elements by index from a very large list?如何通过索引从一个非常大的列表中有效地删除元素?
【发布时间】:2020-08-19 21:29:41
【问题描述】:

我有一个非常大的整数列表(大约 20 亿个元素)和一个带有索引的列表(几千个元素),我需要从第一个列表中删除元素。我目前的方法是遍历第二个列表中的所有索引,将每个索引传递给第一个列表的 RemoveAt() 方法:

indices.Sort();
indices.Reverse();
for (i = 0; i < indices.Count; i++)
{
    largeList.RemoveAt(indices[i]);
}

但是,大约需要 2 分钟才能完成。我真的需要更快地执行此操作。有什么办法可以优化吗?

我有一个 10 核的 Intel i9X CPU,所以也许有某种并行处理方式?

【问题讨论】:

  • 那么...这个 20 亿项 列表 - 它是在内存中还是在磁盘上?此操作的预期结果是什么 - 一个新列表?磁盘上的文件?
  • 这是一个很大的列表...我的意思是:即使启用了 GC-large-objects,您也已接近向量限制。这些数据有什么特点?独特?排序?顺序重要吗?可以合理划分吗?如果你可以分区:你可以并行化。否则,也许是一些替代的存储格式......?由于同步问题,您不能“简单地”并行化
  • @Vernou 我认为 Parallel LINQ 不会有帮助 - List&lt;T&gt; 不是线程安全的。
  • 此问题已于 2020 年 10 月上旬受到元影响:meta.stackoverflow.com/questions/401743/…
  • 我会考虑在 2 分钟内迭代超过 20 亿个项目非常快。您认为这里可以接受的速度是多少?一个人可以通过什么客观指标来考虑可接受地解决您的问题的答案?至少在澄清之前投票结束“需要详细信息”。

标签: c# .net list performance generic-list


【解决方案1】:

每次调用RemoveAt() 时,它必须将指定索引之后的每个元素向上移动一个元素。如果在一个非常大的列表中有数千个元素要删除,那将导致很多很多(不必要的)移动。

My thinking 是,如果您可以计算每个移动的开始索引和长度,则可以在单次处理中处理列表,而不会出现重叠移动。这是通过比较要删除的索引列表中的相邻元素来完成的。虽然这确实意味着要构建第三个 List&lt;&gt; 的移动操作来执行,但我希望提前计划的最有效、最小的移动策略最终会得到回报(或者也许有一种方法可以在不分配任何对象的情况下做到这一点目前还没有发生在我身上)。

您可以在下面的基准代码中看到我的实现,RemoveUsingMoveStrategy()。在test 模式下运行下面的启动程序,我们可以看到它——以及其他答案——在给定索引01510111515(重复)、181920 元素中删除 List&lt;int&gt;...

PS> dotnet run --configuration release --framework netcoreapp3.1 --test
[剪辑]
移除使用移动策略():
        元素:{ 2, 3, 4, 6, 7, 8, 9, 12, 13, 14, 16, 17 }
           计数:12
        序列:等于控制列表
        持续时间:3.95 毫秒
        返回:输入列表
[剪辑]

基准说明

  • 我打算以十亿元素 List&lt;int&gt; 为基准,但 RemoveUsingRemoveAt()(受问题中的代码启发)如此效率太低,以至于花费了太长时间,所以我只增加了 1000 万个元素。

  • 我仍然希望运行一个不包括RemoveUsingRemoveAt() 的十亿元素基准测试。出于这个原因,我引入了一个名为RemoveUsingListCopy() 的不太天真的实现,作为比较所有列表大小的基线。顾名思义,它不会修改输入列表,而是创建一个应用了删除的新列表。

  • 由于并非所有实现都要求要删除的索引列表是唯一的和/或排序的,所以我认为最公平的基准测试方法是在方法需要的基准时间中包含该开销它。

  • 我对其他答案中的代码所做的唯一其他更改是利用基准列表(DataListRemovalList)并将 var 更改为显式类型以使读者清楚。

  • RemovalListLocation 表示从哪里删除了DataList 索引。

    • 对于BeginningMiddleEnd,它是从该位置移除的一个连续的RemovalListSize 大小的块。
    • 对于Random,它是RemovalListSize 随机、有效、未排序、不保证唯一的索引,从常量种子生成。

    为了使结果保持简短(呃),我选择只对 Middle 进行基准测试——认为这是一个很好的中间折衷——和 Random 值.

基准测试结果

  • RemoveUsingRemoveAt()可怕。不要那样做。

  • 对于较小的列表,RemoveUsingListCopy() 始终是最快的,但内存使用量增加了一倍。

  • 对于较大的列表,Vernou's answer 的速度至少是其他答案的 5 倍,但代价是依赖实施细节。

    这只是表明您不一定总是偏爱List&lt;&gt; 而不是数组——除非你需要它的额外功能——因为它并不总是优越的。它隔离了底层数组,防止您(无需反射)在其上使用更高性能的访问方法,如 Array.Copy()unsafe 代码。

  • Theodor Zoulias's answer 在较小的列表中表现良好,但我认为必须“触及”所有 1000 万个索引——每个索引都调用HashSet&lt;&gt;.Contains() 并增加一个索引变量——对于较大的列表来说确实伤害了它。

  • 其余实现(包括我的)具有大致相同的性能:不是最好的,但仍然相当不错。

基准数据

benchmark 模式运行此答案后面定义的启动程序,我从BenchmarkDotNet 获得这些结果...

PS> dotnet run --configuration release --framework netcoreapp3.1 -- benchmark
[剪辑]
// * 概括 *

BenchmarkDotNet=v0.12.1,操作系统=Windows 10.0.19041.450 (2004/?/20H1)
Intel Core i7 CPU 860 2.80GHz (Nehalem),1 个 CPU,8 个逻辑核心和 4 个物理核心
.NET 核心 SDK=3.1.401
  [主机]:.NET Core 3.1.7(CoreCLR 4.700.20.36602,CoreFX 4.700.20.37001),X64 RyuJIT
  中运行:.NET Framework 4.8 (4.8.4200.0),X64 RyuJIT

Job=MediumRun InvocationCount=1 IterationCount=15
LaunchCount=2 UnrollFactor=1 WarmupCount=10

|方法 |运行时 |数据列表大小 |删除列表大小 |删除列表位置 |平均值 |错误 |标准差 |中位数 |比率 |比率SD |
|------------------------------ |-------------- |--- ---------- |---------------- |-------- |- --------------:|--------------:|--------------:|-- ----------:|--------:|--------:|
| RemoveUsingListCopy | .NET 4.8 | 10000 | 2000 |随机 | 379.9 微秒 | 15.93 微秒 | 23.36 微秒 | 380.4 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 4.8 | 10000 | 2000 |随机 | 1,463.7 微秒 | 93.79 微秒 | 137.47 微秒 | 1,434.7 微秒 | 3.87 | 0.41 |
| RemoveUsingMoveStrategy | .NET 4.8 | 10000 | 2000 |随机 | 635.9 微秒 | 19.77 微秒 | 29.60 微秒 | 624.0 微秒 | 1.67 | 0.14 |
| Answer63496225_TheodorZoulias | .NET 4.8 | 10000 | 2000 |随机 | 372.1 微秒 | 11.52 微秒 | 16.15 微秒 | 373.8 微秒 | 0.99 | 0.07 |
| Answer63496768_CoolBots | .NET 4.8 | 10000 | 2000 |随机 | 594.5 微秒 | 25.13 微秒 | 36.03 微秒 | 593.1 微秒 | 1.57 | 0.12 |
| Answer63495657_NetMage | .NET 4.8 | 10000 | 2000 |随机 | 618.8 微秒 | 17.53 微秒 | 23.99 微秒 | 622.4 微秒 | 1.65 | 0.13 |
| Answer63496256_Vernou | .NET 4.8 | 10000 | 2000 |随机 | 645.6 微秒 | 27.28 微秒 | 39.99 微秒 | 632.2 微秒 | 1.71 | 0.16 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 核心 3.1 | 10000 | 2000 |随机 | 391.5 微秒 | 10.39 微秒 | 15.55 微秒 | 391.6 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 核心 3.1 | 10000 | 2000 |随机 | 1,402.2 微秒 | 44.20 微秒 | 64.80 微秒 | 1,407.6 微秒 | 3.59 | 0.21 |
| RemoveUsingMoveStrategy | .NET 核心 3.1 | 10000 | 2000 |随机 | 557.9 微秒 | 19.73 微秒 | 27.00 微秒 | 557.2 微秒 | 1.43 | 0.10 |
| Answer63496225_TheodorZoulias | .NET 核心 3.1 | 10000 | 2000 |随机 | 424.3 微秒 | 20.90 微秒 | 29.30 微秒 | 424.2 微秒 | 1.09 | 0.09 |
| Answer63496768_CoolBots | .NET 核心 3.1 | 10000 | 2000 |随机 | 535.0 微秒 | 19.37 微秒 | 27.16 微秒 | 537.1 微秒 | 1.37 | 0.08 |
| Answer63495657_NetMage | .NET 核心 3.1 | 10000 | 2000 |随机 | 557.7 微秒 | 18.73 微秒 | 25.63 微秒 | 550.0 微秒 | 1.43 | 0.09 |
| Answer63496256_Vernou | .NET 核心 3.1 | 10000 | 2000 |随机 | 554.2 微秒 | 13.82 微秒 | 18.45 微秒 | 554.0 微秒 | 1.42 | 0.07 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 4.8 | 10000 | 2000 |中 | 221.6 微秒 | 7.25 微秒 | 10.63 微秒 | 222.5 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 4.8 | 10000 | 2000 |中 | 1,195.3 微秒 | 20.01 微秒 | 28.69 微秒 | 1,187.7 微秒 | 5.42 | 0.30 |
| RemoveUsingMoveStrategy | .NET 4.8 | 10000 | 2000 |中 | 405.0 微秒 | 13.65 微秒 | 19.14 微秒 | 410.7 微秒 | 1.83 | 0.10 |
| Answer63496225_TheodorZoulias | .NET 4.8 | 10000 | 2000 |中 | 206.3 微秒 | 8.62 微秒 | 12.09 微秒 | 204.9 微秒 | 0.94 | 0.08 |
| Answer63496768_CoolBots | .NET 4.8 | 10000 | 2000 |中 | 427.5 微秒 | 15.56 微秒 | 22.81 微秒 | 435.4 微秒 | 1.93 | 0.13 |
| Answer63495657_NetMage | .NET 4.8 | 10000 | 2000 |中 | 405.4 微秒 | 13.80 微秒 | 19.35 微秒 | 403.8 微秒 | 1.84 | 0.11 |
| Answer63496256_Vernou | .NET 4.8 | 10000 | 2000 |中 | 413.9 微秒 | 15.26 微秒 | 20.89 微秒 | 419.8 微秒 | 1.87 | 0.12 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 核心 3.1 | 10000 | 2000 |中 | 235.2 微秒 | 10.73 微秒 | 15.73 微秒 | 236.2 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 核心 3.1 | 10000 | 2000 |中 | 1,345.6 微秒 | 32.07 微秒 | 43.90 微秒 | 1,352.7 微秒 | 5.77 | 0.41 |
| RemoveUsingMoveStrategy | .NET 核心 3.1 | 10000 | 2000 |中 | 324.0 微秒 | 4.92 微秒 | 7.05 微秒 | 326.6 微秒 | 1.39 | 0.09 |
| Answer63496225_TheodorZoulias | .NET 核心 3.1 | 10000 | 2000 |中 | 262.9 微秒 | 6.18 微秒 | 9.06 微秒 | 265.4 微秒 | 1.12 | 0.08 |
| Answer63496768_CoolBots | .NET 核心 3.1 | 10000 | 2000 |中 | 333.6 微秒 | 10.14 微秒 | 13.87 微秒 | 340.9 微秒 | 1.43 | 0.11 |
| Answer63495657_NetMage | .NET 核心 3.1 | 10000 | 2000 |中 | 313.5 微秒 | 9.05 微秒 | 12.69 微秒 | 310.5 微秒 | 1.34 | 0.11 |
| Answer63496256_Vernou | .NET 核心 3.1 | 10000 | 2000 |中 | 332.3 微秒 | 6.70 微秒 | 8.95 微秒 | 331.9 微秒 | 1.43 | 0.09 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 4.8 | 10000000 | 2000 |随机 | 253,977.1 微秒 | 2,721.70 微秒 | 3,989.43 微秒 | 253,809.0 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 4.8 | 10000000 | 2000 |随机 | 5,191,083.4 微秒 | 13,200.66 微秒 | 18,931.99 微秒 | 5,187,162.3 微秒 | 20.43 | 0.34 |
| RemoveUsingMoveStrategy | .NET 4.8 | 10000000 | 2000 |随机 | 65,365.4 微秒 | 422.41 微秒 | 592.16 微秒 | 65,307.3 微秒 | 0.26 | 0.00 |
| Answer63496225_TheodorZoulias | .NET 4.8 | 10000000 | 2000 |随机 | 240,584.4 微秒 | 3,687.89 微秒 | 5,048.03 微秒 | 244,336.1 微秒 | 0.95 | 0.02 |
| Answer63496768_CoolBots | .NET 4.8 | 10000000 | 2000 |随机 | 54,168.4 微秒 | 1,001.37 微秒 | 1,436.14 微秒 | 53,390.3 微秒 | 0.21 | 0.01 |
| Answer63495657_NetMage | .NET 4.8 | 10000000 | 2000 |随机 | 72,501.4 微秒 | 452.46 微秒 | 634.29 微秒 | 72,161.2 微秒 | 0.29 | 0.00 |
| Answer63496256_Vernou | .NET 4.8 | 10000000 | 2000 |随机 | 5,814.0 微秒 | 89.71 微秒 | 128.67 微秒 | 5,825.3 微秒 | 0.02 | 0.00 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 核心 3.1 | 10000000 | 2000 |随机 | 239,784.0 微秒 | 2,721.35 微秒 | 3,902.88 微秒 | 241,125.5 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 核心 3.1 | 10000000 | 2000 |随机 | 5,538,337.5 微秒 | 353,505.30 微秒 | 495,565.06 微秒 | 5,208,226.1 微秒 | 23.12 | 2.15 |
| RemoveUsingMoveStrategy | .NET 核心 3.1 | 10000000 | 2000 |随机 | 33,071.8 微秒 | 103.80 微秒 | 138.57 微秒 | 33,030.5 微秒 | 0.14 | 0.00 |
| Answer63496225_TheodorZoulias | .NET 核心 3.1 | 10000000 | 2000 |随机 | 240,825.5 微秒 | 851.49 微秒 | 1,248.11 微秒 | 240,520.9 微秒 | 1.00 | 0.02 |
| Answer63496768_CoolBots | .NET 核心 3.1 | 10000000 | 2000 |随机 | 26,265.0 微秒 | 90.76 微秒 | 124.23 微秒 | 26,253.0 微秒 | 0.11 | 0.00 |
| Answer63495657_NetMage | .NET 核心 3.1 | 10000000 | 2000 |随机 | 48,670.6 微秒 | 581.51 微秒 | 833.99 微秒 | 48,303.0 微秒 | 0.20 | 0.00 |
| Answer63496256_Vernou | .NET 核心 3.1 | 10000000 | 2000 |随机 | 5,905.5 微秒 | 96.27 微秒 | 131.78 微秒 | 5,915.1 微秒 | 0.02 | 0.00 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 4.8 | 10000000 | 2000 |中 | 153,776.2 微秒 | 2,454.90 微秒 | 3,674.38 微秒 | 152,872.0 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 4.8 | 10000000 | 2000 |中 | 5,245,952.0 微秒 | 13,845.58 微秒 | 20,294.67 微秒 | 5,252,922.4 微秒 | 34.10 | 0.81 |
| RemoveUsingMoveStrategy | .NET 4.8 | 10000000 | 2000 |中 | 33,233.6 微秒 | 110.33 微秒 | 158.24 微秒 | 33,217.3 微秒 | 0.22 | 0.01 |
| Answer63496225_TheodorZoulias | .NET 4.8 | 10000000 | 2000 |中 | 128,949.8 微秒 | 560.72 微秒 | 804.17 微秒 | 128,724.9 微秒 | 0.84 | 0.02 |
| Answer63496768_CoolBots | .NET 4.8 | 10000000 | 2000 |中 | 48,965.1 微秒 | 70.75 微秒 | 94.45 微秒 | 48,957.3 微秒 | 0.32 | 0.01 |
| Answer63495657_NetMage | .NET 4.8 | 10000000 | 2000 |中 | 32,641.5 微秒 | 66.85 微秒 | 91.51 微秒 | 32,610.0 微秒 | 0.21 | 0.01 |
| Answer63496256_Vernou | .NET 4.8 | 10000000 | 2000 |中 | 2,982.2 微秒 | 29.47 微秒 | 41.31 微秒 | 2,961.9 微秒 | 0.02 | 0.00 |
| | | | | | | | | | | |
| RemoveUsingListCopy | .NET 核心 3.1 | 10000000 | 2000 |中 | 144,208.7 微秒 | 2,035.88 微秒 | 2,984.16 微秒 | 142,693.2 微秒 | 1.00 | 0.00 |
| RemoveUsingRemoveAt | .NET 核心 3.1 | 10000000 | 2000 |中 | 5,235,957.7 微秒 | 13,674.19 微秒 | 20,043.46 微秒 | 5,241,536.1 微秒 | 36.32 | 0.78 |
| RemoveUsingMoveStrategy | .NET 核心 3.1 | 10000000 | 2000 |中 | 16,547.3 微秒 | 72.72 微秒 | 101.95 微秒 | 16,520.7 微秒 | 0.11 | 0.00 |
| Answer63496225_TheodorZoulias | .NET 核心 3.1 | 10000000 | 2000 |中 | 137,218.2 微秒 | 716.45 微秒 | 980.69 微秒 | 137,027.0 微秒 | 0.95 | 0.02 |
| Answer63496768_CoolBots | .NET 核心 3.1 | 10000000 | 2000 |中 | 23,728.5 微秒 | 79.84 微秒 | 111.93 微秒 | 23,689.9 微秒 | 0.16 | 0.00 |
| Answer63495657_NetMage | .NET 核心 3.1 | 10000000 | 2000 |中 | 17,298.3 微秒 | 216.46 微秒 | 310.44 微秒 | 17,165.5 微秒 | 0.12 | 0.00 |
| Answer63496256_Vernou | .NET 核心 3.1 | 10000000 | 2000 |中 | 2,999.7 微秒 | 85.78 微秒 | 123.03 微秒 | 2,957.1 微秒 | 0.02 | 0.00 |
[剪辑]

基准代码

要查看各种实现,向下滚动三分之一并查找带有[Benchmark()] 修饰的方法。

这需要BenchmarkDotNet package

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BenchmarkDotNet.Attributes;

namespace SO63495264
{
    public class Benchmarks
    {
        public enum RemovalLocation
        {
            Random,
            Beginning,
            Middle,
            End
        }

        [Params(10_000, 10_000_000)]
        public int DataListSize
        {
            get; set;
        }

        [Params(2_000)]
        public int RemovalListSize
        {
            get; set;
        }

        //[ParamsAllValues()]
        [Params(RemovalLocation.Random, RemovalLocation.Middle)]
        public RemovalLocation RemovalListLocation
        {
            get; set;
        }

        public List<int> DataList
        {
            get;
            set;
        }

        public IReadOnlyList<int> RemovalList
        {
            get;
            set;
        }

        [GlobalSetup()]
        public void GlobalSetup()
        {
            IEnumerable<int> removalIndices;

            if (RemovalListLocation == RemovalLocation.Random)
            {
                // Pass a constant seed so the same indices get generated for every run
                Random random = new Random(12345);

                removalIndices = Enumerable.Range(0, RemovalListSize)
                    .Select(i => random.Next(DataListSize));
            }
            else
            {
                int removalSegmentOffset = RemovalListLocation switch {
                    RemovalLocation.Beginning => 0,
                    RemovalLocation.Middle => (DataListSize - RemovalListSize) / 2,
                    RemovalLocation.End => DataListSize - RemovalListSize,
                    _ => throw new Exception($"Unexpected {nameof(RemovalLocation)} enumeration value {RemovalListLocation}.")
                };

                removalIndices = Enumerable.Range(removalSegmentOffset, RemovalListSize);
            }

            // For efficiency, create a single List<int> to be reused by all iterations for a given set of parameters
            DataList = new List<int>(DataListSize);
            RemovalList = removalIndices.ToList().AsReadOnly();
        }

        [IterationSetup()]
        public void IterationSetup()
        {
            // Each iteration could modify DataList, so repopulate its elements for the
            // next iteration.  DataList is either new or has been Clear()ed by IterationCleanup().
            for (int i = 0; i < DataListSize; i++)
                DataList.Add(i);
        }

        [IterationCleanup()]
        public void IterationCleanup()
        {
            DataList.Clear();
        }

        [GlobalCleanup()]
        public void GlobalCleanup()
        {
            // Force collection of the List<> for the current set of parameters
            int generation = GC.GetGeneration(DataList);

            DataList = null;
            GC.Collect(generation, GCCollectionMode.Forced, true);
        }

        [Benchmark(Baseline = true)]
        public List<int> RemoveUsingListCopy()
        {
            HashSet<int> removalSet = RemovalList.ToHashSet();
            List<int> newList = new List<int>(DataList.Count - removalSet.Count);

            for (int index = 0; index < DataList.Count; index++)
                if (!removalSet.Contains(index))
                    newList.Add(DataList[index]);

            return newList;
        }

        // Based on Stack Overflow question 63495264
        // https://stackoverflow.com/q/63495264/150605
        [Benchmark()]
        public List<int> RemoveUsingRemoveAt()
        {
            // The collection of indices to remove must be in decreasing order
            // with no duplicates; all are assumed to be in-range for DataList
            foreach (int index in RemovalList.Distinct().OrderByDescending(i => i))
                DataList.RemoveAt(index);

            return DataList;
        }

        // From Stack Overflow answer 63498191
        // https://stackoverflow.com/a/63498191/150605
        [Benchmark()]
        public List<int> RemoveUsingMoveStrategy()
        {
            // The collection of indices to remove must be in increasing order
            // with no duplicates; all are assumed to be in-range for DataList
            int[] coreIndices = RemovalList.Distinct().OrderBy(i => i).ToArray();
            List<(int startIndex, int count, int distance)> moveOperations
                = new List<(int, int, int)>();

            int candidateIndex;
            for (int i = 0; i < coreIndices.Length; i = candidateIndex)
            {
                int currentIndex = coreIndices[i];
                int elementCount = 1;

                // Merge removal of consecutive indices into one operation
                while ((candidateIndex = i + elementCount) < coreIndices.Length
                    && coreIndices[candidateIndex] == currentIndex + elementCount
                )
                {
                    elementCount++;
                }

                int nextIndex = candidateIndex < coreIndices.Length
                    ? coreIndices[candidateIndex]
                    : DataList.Count;
                int moveCount = nextIndex - currentIndex - elementCount;

                // Removing one or more elements from the end of the
                // list will result in a no-op, 0-length move; omit it
                if (moveCount > 0)
                {
                    moveOperations.Add(
                        (
                            startIndex: currentIndex + elementCount,
                            count: moveCount,
                            distance: i + elementCount
                        )
                    );
                }
            }

            // Perform the element moves
            foreach ((int startIndex, int count, int distance) in moveOperations)
            {
                for (int offset = 0; offset < count; offset++)
                {
                    int sourceIndex = startIndex + offset;
                    int targetIndex = sourceIndex - distance;

                    DataList[targetIndex] = DataList[sourceIndex];
                }
            }

            // "Trim" the number of removed elements from the end of the list
            DataList.RemoveRange(DataList.Count - coreIndices.Length, coreIndices.Length);

            return DataList;
        }

        // Adapted from Stack Overflow answer 63496225 revision 4
        // https://stackoverflow.com/revisions/63496225/4
        [Benchmark()]
        public List<int> Answer63496225_TheodorZoulias()
        {
            HashSet<int> indicesSet = new HashSet<int>(RemovalList);
            int index = 0;

            DataList.RemoveAll(_ => indicesSet.Contains(index++));

            return DataList;
        }


        // Adapted from Stack Overflow answer 63496768 revision 2
        // https://stackoverflow.com/revisions/63496768/2
        [Benchmark()]
        public List<int> Answer63496768_CoolBots()
        {
            List<int> sortedIndicies = RemovalList.Distinct().OrderBy(i => i).ToList();

            int sourceStartIndex = 0;
            int destStartIndex = 0;
            int spanLength = 0;

            int skipCount = 0;

            // Copy items up to last index to be skipped
            foreach (int skipIndex in sortedIndicies)
            {
                spanLength = skipIndex - sourceStartIndex;
                destStartIndex = sourceStartIndex - skipCount;

                for (int i = sourceStartIndex; i < sourceStartIndex + spanLength; i++)
                {
                    DataList[destStartIndex] = DataList[i];
                    destStartIndex++;
                }

                sourceStartIndex = skipIndex + 1;
                skipCount++;
            }

            // Copy remaining items (between last index to be skipped and end of list)
            spanLength = DataList.Count - sourceStartIndex;
            destStartIndex = sourceStartIndex - skipCount;

            for (int i = sourceStartIndex; i < sourceStartIndex + spanLength; i++)
            {
                DataList[destStartIndex] = DataList[i];
                destStartIndex++;
            }

            DataList.RemoveRange(destStartIndex, sortedIndicies.Count);

            return DataList;
        }

        // Adapted from Stack Overflow answer 63495657 revision 6
        // https://stackoverflow.com/revisions/63495657/6
        [Benchmark()]
        public List<int> Answer63495657_NetMage()
        {
            List<int> removeAtList = RemovalList.Distinct().OrderBy(i => i).ToList();

            int srcCount = DataList.Count;
            int ralCount = removeAtList.Count;
            int removeAtIndice = 1;
            int freeIndex = removeAtList[0];
            int current = freeIndex + 1;
            while (current < srcCount)
            {
                while (removeAtIndice < ralCount && current == removeAtList[removeAtIndice])
                {
                    ++current;
                    ++removeAtIndice;
                }

                if (current < srcCount)
                    DataList[freeIndex++] = DataList[current++];
            }

            DataList.RemoveRange(freeIndex, srcCount - freeIndex);

            return DataList;
        }

        // Adapted from Stack Overflow answer 63496256 revision 3
        // https://stackoverflow.com/revisions/63496256/3
        [Benchmark()]
        public List<int> Answer63496256_Vernou()
        {
            List<int> indices = RemovalList.Distinct().OrderBy(i => i).ToList();

            //Get the internal array
            int[] largeArray = (int[]) typeof(List<int>)
                .GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic)
                .GetValue(DataList);

            int current = 0;
            int copyFrom = 0;

            for (int i = 0; i < indices.Count; i++)
            {
                int copyTo = indices[i];
                if (copyTo < copyFrom)
                {
                    //In case the indice is duplicate,
                    //The item is already passed
                    continue;
                }
                int copyLength = copyTo - copyFrom;
                Array.Copy(largeArray, copyFrom, largeArray, current, copyLength);
                current += copyLength;
                copyFrom = copyTo + 1;
            }
            //Resize the internal array
            DataList.RemoveRange(DataList.Count - indices.Count, indices.Count);

            return DataList;
        }
    }
}

启动器代码

RunBenchmark() 定义要运行的基准作业的类型以及运行时。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;

namespace SO63495264
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args?.Length == 0)
            {
                string assemblyFilePath = Assembly.GetExecutingAssembly().Location;
                string assemblyFileName = System.IO.Path.GetFileName(assemblyFilePath);

                Console.WriteLine($"{assemblyFileName} {{ benchmark | test }}");
            }
            else if (string.Equals(args[0], "benchmark", StringComparison.OrdinalIgnoreCase))
                RunBenchmark();
            else if (string.Equals(args[0], "test", StringComparison.OrdinalIgnoreCase))
                RunTest();
            else
                Console.WriteLine($"Unexpected parameter \"{args[0]}\"");
        }

        static void RunBenchmark()
        {
            Job baseJob = Job.MediumRun;
            IConfig config = DefaultConfig.Instance;

            foreach (Runtime runtime in new Runtime[] { CoreRuntime.Core31, ClrRuntime.Net48 })
            {
                config = config.AddJob(
                    baseJob.WithRuntime(runtime)
                );
            }

            BenchmarkDotNet.Running.BenchmarkRunner.Run<Benchmarks>(config);
        }

        static void RunTest()
        {
            const int ListSize = 20;
            const int MaxDisplayElements = 20;

            IEnumerable<int> data = Enumerable.Range(0, ListSize);
            IReadOnlyList<int> indices = new List<int>(
                new int[] {
                    0,                               1, // First two indices
                    ListSize / 4,
                    ListSize / 2,     ListSize / 2 + 1, // Consecutive indices
                    ListSize / 4 * 3, ListSize / 4 * 3, // Duplicate indices
                    ListSize - 2,     ListSize - 1      // Last two indices
                }
            ).AsReadOnly();

            // Discover and invoke the benchmark methods the same way BenchmarkDotNet would
            Benchmarks benchmarks = new Benchmarks() {
                RemovalList = indices
            };
            IEnumerable<MethodInfo> benchmarkMethods = benchmarks.GetType()
                .GetMethods(BindingFlags.Instance | BindingFlags.Public)
                .Where(
                    method => method.CustomAttributes.Any(
                        attribute => attribute.AttributeType == typeof(BenchmarkAttribute)
                    )
                );

            // Call a known-good method to get the correct results for comparison
            benchmarks.DataList = data.ToList();
            List<int> controlList = benchmarks.RemoveUsingListCopy();

            foreach (MethodInfo benchmarkMethod in benchmarkMethods)
            {
                List<int> inputList = data.ToList();

                benchmarks.DataList = inputList;
                Stopwatch watch = Stopwatch.StartNew();
                List<int> outputList = (List<int>) benchmarkMethod.Invoke(benchmarks, Array.Empty<object>());
                watch.Stop();

                Console.WriteLine($"{benchmarkMethod.Name}():");
                Console.WriteLine($"\tElements: {{ {string.Join(", ", outputList.Take(MaxDisplayElements))}{(outputList.Count > MaxDisplayElements ? ", ..." : "") } }}");
                Console.WriteLine($"\t   Count: {outputList.Count:N0}");
                Console.WriteLine($"\tSequence: {(outputList.SequenceEqual(controlList) ? "E" : "*Not* e")}qual to control list");
                Console.WriteLine($"\tDuration: {watch.Elapsed.TotalMilliseconds:N2} milliseconds");
                Console.WriteLine($"\tReturned: {(object.ReferenceEquals(outputList, inputList) ? "Input list" : "New list")}");
            }
        }
    }
}

为了在 .NET Framework (4.8) 上进行基准测试,我必须将以下属性添加到我的 .NET Core .csproj 项目文件中:

<TargetFrameworks>netcoreapp3.1;net48</TargetFrameworks>
<LangVersion>8.0</LangVersion>

41 个字符 备用!

【讨论】:

    【解决方案2】:

    由于源列表的顺序很重要,您可以将每个项目向下移动,跳过要删除的索引,然后删除列表的末尾。

    更新:获取 RemoveAll 的 .Net Core 源代码并将其修改为索引列表而不是谓词。

    更新 2:优化为尽可能不重复测试。

    更新 3:简化为在基准测试中证明额外代码的优化速度较慢。

    src 作为大列表,将removeAtList 作为索引以随机顺序删除,您可以这样做:

    removeAtList.Sort();
    
    var srcCount = src.Count;
    var ralCount = removeAtList.Count;
    var removeAtIndice = 1;
    var freeIndex = removeAtList[0];
    var current = freeIndex+1;
    while (current < srcCount) {
        while (removeAtIndice < ralCount && current == removeAtList[removeAtIndice]) {
            ++current;
            ++removeAtIndice;
        }
    
        if (current < srcCount)
            src[freeIndex++] = src[current++];
    }
    
    src.RemoveRange(freeIndex, srcCount-freeIndex);
    

    对于 10 亿个随机整数元素列表和 1000 - 3000 个随机索引元素列表,使用此算法每次删除我得到 1.1 毫秒。使用RemoveAt,我每​​次删除的时间超过 232.77 毫秒,因此速度快了大约 200 倍。

    【讨论】:

    • 那么,您是说您的 1_000_000_000 值的总运行时间,比如说,要删除的 2000 个指标是 1.14 毫秒 x 2000 = 2.28 秒?你的解决方案大约需要 26 秒......这仍然比我迄今为止想出的任何东西都要快,哈哈!
    • @CoolBots 是的 - 创建列表大约需要 12 秒,然后删除 2000 个索引大约需要 3.2 秒 - 在 LINQPad 中使用 .Net Core 3.1 我的每毫秒时间从 1.2 到 3 毫秒6.
    • @CoolBots 我通过修改 .Net Core 3.1 List&lt;T&gt;.RemoveAll 中的代码以获取跳过索引列表并进行优化,从而更新了我的答案。 (PS LINQPad 6 x64。)
    • 它比以前快了,但我无法达到您声称的时间。我的版本运行时间约为 12 秒。我刚刚想出了一个在 5 秒内运行的解决方案,将发布。
    • @CoolBots 不错。这也可能是因为我在 Windows 10 x64 下运行在具有 32GB RAM 的 i7-4790
    【解决方案3】:

    实现并行化的一种方法是将列表分成多个片段;也许最初(任意)分开一百万个元素的平板。只要每个slab保持自己的计数,您就可以按索引将工作拆分为来自不同slab的移除(纯粹基于计数),然后同时执行实际的移除工作。如果您在每个中保留一些备用容量,您还可以更便宜地在中间添加元素,因为您通常只接触一个平板。随机访问会慢一些,因为您可能需要查看多个slab 计数以确定正确的slab,但如果slab 计数保持在一个连续的向量中(而不是针对每个slab),您应该有很好的内存缓存命中在做的时候。

    【讨论】:

    • 或者用相同的扁平线性数据结构并行化:如果你有一个排序的唯一索引列表要删除,你可以计算给定输入索引的输出索引:从indices[i]开始,有在此之前将被i 删除。如果indices[] 很小,您可以扫描它以跨线程分配块复制工作,并将所有线程复制到单个数组中。
    【解决方案4】:

    此答案基于此处的其他答案-主要是,正如@Vernou(在他们的答案中)和@BACON(在cmets中)所建议的那样,我正在将列表中的元素向上移动。这个最终是高性能的(不像我的前几种方法),并且比迄今为止发布的其他解决方案更快,至少在我的测试中 - 我尝试了 OP 的 2_000_000_000 条目和 2_000 指标的设置 - 运行时间不到 10 秒我的笔记本电脑(i7-8550U @ 1.8GHz,16GB RAM):

    static void FilterOutIndicies(List<int> values, List<int> sortedIndicies)
    {
        int sourceStartIndex = 0;
        int destStartIndex = 0;
        int spanLength = 0;
    
        int skipCount = 0;
    
        // Copy items up to last index to be skipped
        foreach (var skipIndex in sortedIndicies)
        {
            spanLength = skipIndex - sourceStartIndex;
            destStartIndex = sourceStartIndex - skipCount;
    
            for (int i = sourceStartIndex; i < sourceStartIndex + spanLength; i++)
            {
                values[destStartIndex] = values[i];
                destStartIndex++;
            }
    
            sourceStartIndex = skipIndex + 1;
            skipCount++;
        }
    
        // Copy remaining items (between last index to be skipped and end of list)
        spanLength = values.Count - sourceStartIndex;
        destStartIndex = sourceStartIndex - skipCount;
    
        for (int i = sourceStartIndex; i < sourceStartIndex + spanLength; i++)
        {
            values[destStartIndex] = values[i];
            destStartIndex++;
        }
    
        values.RemoveRange(destStartIndex, sortedIndicies.Count);
    }
    

    【讨论】:

    • 使用固定的Random,我的得到 1.22 毫秒,你的得到 0.86 毫秒,非常好!
    • 快速检查表明我的 CPU 应该比你的快 23%。
    • @NetMage 感谢您在您的 PC 上检查我的版本。是的,由于阵列大小,我认为真正有帮助的是更快的 CPU 和双倍的 RAM。也许是时候为我升级了,哈哈!
    • 在我的电脑上,NetMage 解决方案需要 22 秒,CoolBots 解决方案需要 15 秒。我的解决方案需要 2 秒,但我使用反射。所有解决方案在内存使用方面都是相同的。
    • 半最后的“复制剩余项目”步骤已过时。您只需为RemoveRange 调用使用正确的范围。
    【解决方案5】:

    List.RemoveAt 方法从删除的项目中复制所有下一个项目。 在你的情况下,这个副本 2,000 * 2,000,000,000 次每个项目(不是真的,但真的很接近)。

    解决方案是在已删除项目和下一个已删除项目之间手动复制项目:

    static void Main(string[] args)
    {
        var largeList = Enumerable.Range(0, 2_000_000_000).ToList();
    
        var indices = new List<int>();
        var rand = new Random();
        for (var i = 0; i < 20000; i++)
        {
            indices.Add(rand.Next(0, largeList.Count - 1));
        }
        indices.Sort();
    
        var watch = new Stopwatch();
        watch.Start();
    
        // You can convert the list to array with ToArray,
        // but this duplicate the memory use.
        // Or get the internal array by reflection,
        // but reflection on external library isn't recommended
        var largeArray = (int[])typeof(List<int>)
            .GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic)
            .GetValue(largeList);
    
        var current = 0;
        var copyFrom = 0;
    
        for (var i = 0; i < indices.Count; i++)
        {
            var copyTo = indices[i];
            if (copyTo < copyFrom)
            {
                //In case the indice is duplicate,
                //The item is already passed
                continue;
            }
            var copyLength = copyTo - copyFrom;
            Array.Copy(largeArray, copyFrom, largeArray, current, copyLength);
            current += copyLength;
            copyFrom = copyTo + 1;
        }
        //Resize the internal array
        largeList.RemoveRange(largeList.Count - indices.Count, indices.Count);
    
        watch.Stop();
        Console.WriteLine(watch.Elapsed);
        Console.WriteLine(largeList.Count);
    }
    

    【讨论】:

    • @Alexei-LevenkovList 封装了Array。我编辑了答案以获取内部数组。
    • @Vernou 这是一个非常有趣的方法。百感交集,哈哈,但绝对有趣!
    • @CoolBots 我认为这是加快你速度的唯一方法,但我绝对不会在生产中使用它:)
    • 我已更新my answer 中的基准测试以包含您的代码。对于我进行基准测试的更大(1000 万个元素)列表,其他答案通常需要基线实施时间的 10-20%;你的花费了其他答案的 5-10% 的时间(即基线的 2%)。
    【解决方案6】:

    如果您要从 List 中删除多个项目,并且无法将 List 替换为新的 List,则最有效的方法是使用 RemoveAll 方法而不是 @987654330 @。 RemoveAll 只重新排列 List 的内部状态一次,而不是每删除一个项目就重新排列一次。

    RemoveAll 接受一个Predicate&lt;T&gt;,它将为列表(大列表)中的每个项目调用一次。不幸的是,这个委托没有收到当前测试项目的索引。但是,您可以依赖于了解RemoveAll 是如何实现的。 source code reveals 项目按升序依次测试。因此,基于这些知识,您可以使用以下三行非常有效地从列表中删除选定的索引:

    var indicesSet = new HashSet<int>(indices);
    int index = 0;
    largeList.RemoveAll(_ => indicesSet.Contains(index++));
    

    但你真的不应该。如果 .NET 的未来版本带有 RemoveAll 的不同内部实现,则此解决方案将严重崩溃。所以认为这是一个肮脏的黑客,而不是解决问题的生产质量解决方案。

    【讨论】:

    • 不会工作 - Predicate&lt;T&gt; 正在使用 items 而不是 indexes
    • @AlexeiLevenkov 确实如此!我修正了我的答案,但不是很好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-23
    • 2012-08-03
    • 1970-01-01
    • 2010-10-12
    相关资源
    最近更新 更多