【问题标题】:Why is removing by index from an IList performing so much worse than removing by item from an ISet?为什么从 IList 中按索引删除比从 ISet 按项删除要差得多?
【发布时间】:2012-11-24 18:13:02
【问题描述】:

编辑:我将添加一些基准测试结果。对于列表中的大约 1000 - 5000 个项目,IListRemoveAt 优于 ISetRemove,但这并不值得担心,因为差异很小。当收藏规模扩大到 10000 或更多时,真正的乐趣就开始了。我只发布那些数据

我昨晚在回复a question here,遇到了一个奇怪的情况。

先一组简单的方法:

static Random rnd = new Random();
public static int GetRandomIndex<T>(this ICollection<T> source)
{
    return rnd.Next(source.Count);
}

public static T GetRandom<T>(this IList<T> source)
{
    return source[source.GetRandomIndex()];
}

------------------------------------------ -------------------------------------------------- --------------------------------------

假设我从一个集合中随机删除 N 个项目。我会写这个函数:

public static void RemoveRandomly1<T>(this ISet<T> source, int countToRemove)
{
    int countToRemain = source.Count - countToRemove; 
    var inList = source.ToList();

    int i = 0;
    while (source.Count > countToRemain)
    {
        source.Remove(inList.GetRandom()); 
        i++;
    }
}

public static void RemoveRandomly2<T>(this IList<T> source, int countToRemove)
{
    int countToRemain = source.Count - countToRemove;

    int j = 0;
    while (source.Count > countToRemain)
    {
        source.RemoveAt(source.GetRandomIndex()); 
        j++; 
    }
}

如您所见,第一个函数是为ISet 编写的,第二个函数是为普通IList 编写的。在第一个函数中,我从ISet按项目删除,并按IList 中的索引删除,我相信这两个都是O(1)。为什么第二个函数的性能比第一个差这么多,尤其是当列表变大时?

赔率(我的看法):

1) 在第一个函数中,ISet 被转换为IList(从IList 中获取随机项),而在第二个函数中没有执行这样的操作。

优势 IList。

2) 在第一个函数中调用GetRandomItem,而在第二个函数中,调用GetRandomIndex,这又少了一步。

虽然微不足道,但优势 IList。

3) 在第一个函数中,随机项是从一个单独的列表中获取的,因此获取的项可能已经从ISet 中删除。这导致在第一个函数的while 循环中进行更多迭代。在第二个函数中,随机索引是从正在迭代的源中获取的,因此永远不会重复迭代。 我已经对此进行了测试并验证了这一点。

i > j 总是,优势IList

我认为这种行为的原因是 List 在添加或删除项目时需要不断调整大小。但显然在其他一些测试中没有。我跑了:

public static void Remove1(this ISet<int> set)
{
    int count = set.Count;
    for (int i = 0; i < count; i++)
    {
        set.Remove(i + 1);
    }
}

public static void Remove2(this IList<int> lst)
{
    for (int i = lst.Count - 1; i >= 0; i--)
    {
        lst.RemoveAt(i);
    }
}

发现第二个函数跑得更快了。

试验台:

var f = Enumerable.Range(1, 100000);

var s = new HashSet<int>(f);
var l = new List<int>(f);

Benchmark(() =>
{
    //some examples...

    s.RemoveRandomly1(2500);
    l.RemoveRandomly2(2500);

    s.Remove1();
    l.Remove2();

}, 1);

public static void Benchmark(Action method, int iterations = 10000)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < iterations; i++)
        method();

    sw.Stop();
    MsgBox.ShowDialog(sw.Elapsed.TotalMilliseconds.ToString());
}

只是想知道这两种结构是怎么回事..谢谢..

结果:

var f = Enumerable.Range(1, 10000);

s.RemoveRandomly1(7500); => 5ms
l.RemoveRandomly2(7500); => 20ms


var f = Enumerable.Range(1, 100000);

s.RemoveRandomly1(7500); => 7ms
l.RemoveRandomly2(7500); => 275ms


var f = Enumerable.Range(1, 1000000);

s.RemoveRandomly1(75000); => 50ms
l.RemoveRandomly2(75000); => 925000ms

不过,对于大多数典型的需求,一个列表就可以了..!

【问题讨论】:

  • 你没有量化你的结果。如果没有一些数字,我们不知道“更快”是什么意思。 (例如,您没有预热虚拟机,因此很难知道这是否是您观察到的行为的原因)
  • @KirkWoll +1,我会在短时间内添加几个。
  • @KirkWoll 我在问题中添加了一些结果..

标签: c# performance list collections hashset


【解决方案1】:

首先,IList 和 ISet 不是任何东西的实现。我可以编写运行方式非常不同的 IList 或 ISet 实现,因此具体的实现才是重要的(在您的情况下为 List 和 HashSet)。

Accessing a List item by index is O(1) 但不是RemoveAt which is O(n) 删除

从末尾删除列表会很快,因为它不需要复制任何内容,它只是递减它的内部计数器,该计数器存储它有多少项目,直到底层数组中的空点数低于阈值,在这一点它将数组复制到一个较小的数组。一旦达到底层数组的最大容量,它就会创建一个两倍大小的新数组并复制元素。如果低于某个阈值,它将创建一个大小为一半的数组并复制元素。它使用长度属性跟踪它的大小,以便未使用的插槽看起来好像它们不存在。

从列表中随机删除意味着它必须复制索引之后的所有数组条目,以便它们向下滑动一个位置,这本质上是相当慢的,特别是当列表的大小变得更大时。如果您有一个包含 100 万个条目的列表,并且您删除了索引 500,000 处的某些内容,则它必须将数组的后半部分复制到一个位置。

【讨论】:

  • 来自 MSDN 文档的 remove 方法:此方法是 O(n) 操作,其中 n 是(计数 - 索引)。
  • 这是一个很好的观察,你有参考吗?我不敢相信IList 在从索引中删除项目时必须在索引之后复制数组。这太不厚道了! :)
  • Remove 可能是,但我正在做RemoveAt。那肯定是 O(1)!
  • 它只是一个动态大小的数组。想一想 - 你如何在数组的随机索引处删除一个项目而不复制它后面的项目?
  • 一旦你达到底层数组的最大容量,它会创建一个两倍大小的新数组并复制元素。如果低于某个阈值,它将创建一个大小为一半的数组并复制元素。它使用长度属性跟踪它的大小,以便未使用的插槽看起来好像它们不存在。
猜你喜欢
  • 1970-01-01
  • 2021-12-06
  • 1970-01-01
  • 1970-01-01
  • 2018-11-04
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
  • 1970-01-01
相关资源
最近更新 更多