【问题标题】:Most efficient way to clone a list into an existing list, minimizing memory reallocation?将列表克隆到现有列表中的最有效方法,最大限度地减少内存重新分配?
【发布时间】:2015-06-12 06:45:14
【问题描述】:

我需要将现有列表克隆到另一个现有列表中。由于环境要求非常高的性能,我需要消除不必要的内存重新分配。

我能想到的最有效的算法如下,它将根据需要增加目标列表的容量以匹配源列表,但不会减少自己的容量。 (此项目可接受的行为。)

    public static void CloneInto(this List<T> source, List<T> destination)
    {
        if (destination.Capacity < source.Capacity)
        {
            /* Increase capacity in destination */
            destination.Capacity = source.Capacity;
        }

        /* Direct copy of items within the limit of both counts */
        for (var i = 0; i < source.Count && i < destination.Count; i++)
        {
            destination[i] = source[i];
        }

        if (source.Count > destination.Count)
        {
            /* Append any extra items from source */
            for (var i = destination.Count; i < source.Count; i++ )
            {
                destination.Add(source[i]);
            }
        } 
        else if (source.Count < destination.Count)
        {
            /* Trim off any extra items from destination */
            while (destination.Count > source.Count)
            {
                destination.RemoveAt(destination.Count - 1);
            }
        }
    }

然而这似乎有很多代码、逻辑和循环。

有没有更有效的方法将列表克隆到现有列表中,同时避免不必要的内存分配?

【问题讨论】:

  • 你在你的环境中测试过 a.Concat(b) 吗?
  • 我不确定 .Concat() 如何适应这个?当 source 有更大的项目时,我想我可以 .Concat() 尾部作为 destination.Concat(source.Skip(destination.Count)) - 这就是你的意思吗?
  • 这似乎是函数签名中的错误设计。你确定这是强制性的吗?例如,将destination.Capacity 设置为比当前值更大的值会导致重新分配和复制列表(使用不需要的、立即覆盖的值)。如果签名会返回目标列表,您可以简单地创建一个副本并在容量不够大时返回该列表。
  • 您测量/基准测试了吗?这些列表一般有多大?这看起来像是一个不明智的优化。不要把时间花在你没有的问题上。
  • 如果原始性能很重要,为什么不放弃List&lt;T&gt; 并使用T[]?运行时直接支持从零开始的一维数组(又名向量),而List&lt;T&gt; 是在此基础上构建的类型(因此可能会有一些开销,尽管很小)。

标签: c# performance list memory-management clone


【解决方案1】:
if (destination.Capacity < source.Capacity)
{
    /* Increase capacity in destination */
    destination.Capacity = source.Capacity;
}

这可能是错误的...source.Capacity 可能比需要的大...

您正在将destination 中已包含的元素“复制”到“新”destination“缓冲区”。这个副本是不必要的,因为 destination 的元素会被丢弃

所以你至少应该:

if (destination.Capacity < source.Count)
{
    /* Increase capacity in destination */
    destination.Clear();
    destination.Capacity = source.Capacity;
}

这样,如果Capacity被更改,则不需要复制任何元素(注意我使用的是Capacity = Count,因为虽然这在短期内会节省内存,但从长远来看它可以导致内存的多次分配)。请注意,我正在检查source.Count,但对于Capacity 的分配,我使用的是相同的source.Capacity。如果多次调用该方法,这可以最大限度地减少 destination 的重新分配。

小优化:

if (destination.Capacity < source.Count)
{
    /* Increase capacity in destination */
    destination.Capacity = 0;
    destination.Capacity = source.Capacity;
}

这是因为List.Clear() 使用Array.Clear(),所以它确实将内部元素归零,而destination.Capacity = 0 被优化为简单地将内部数组重新分配给static _emptyArray

你甚至可以尝试:

public static void CloneInto(this List<T> source, List<T> destination)
{
    destination.Capacity = 0; // or destination.Clear();
    destination.AddRange(source);
}

但是通过查看source code,我认为它比必要的要慢(因为它首先将元素复制到T[] itemsToInsert = new T[count];,所以它复制了两个元素...

然后:

while (destination.Count > source.Count)
{
    destination.RemoveAt(destination.Count - 1);
}

可以优化为:

destination.RemoveRange(source.Count, destination.Count - source.Count);

【讨论】:

  • 我想你的意思是比较destination.Capacity &lt; source.Count并在中间的代码片段中设置destination.Capacity = source.Count
  • @Amit 我决定应用这部分评论。我已经添加了原因的解释。
  • 将容量设置为计数或容量是否会成为未来某个因素的一个因素取决于此功能的使用方式。你可能是对的,如果没有分析,你也可能是错的。也就是说,无论如何,if 条件都是错误的,因为如果destination.Capacity 足够大以容纳source.Count,则没有理由重新分配。
  • @Amit 我明白你的意思了...改变了。
  • xanatos 我同意你的批评。在 source.Count > destination.Capacity 的情况下,最有效的方法可能就是 destination = source.ToList()。
【解决方案2】:

对性能的小测试:

public static List<T> CloneInto<T>(List<T> source, List<T> destination)
{
    if (destination.Capacity < source.Capacity)
    {
        /* Increase capacity in destination */
        destination.Capacity = source.Capacity;
    }

    /* Direct copy of items within the limit of both counts */
    for (var i = 0; i < source.Count && i < destination.Count; i++)
    {
        destination[i] = source[i];
    }

    if (source.Count > destination.Count)
    {
        /* Append any extra items from source */
        for (var i = destination.Count; i < source.Count; i++)
        {
            destination.Add(source[i]);
        }
    }
    else if (source.Count < destination.Count)
    {
        /* Trim off any extra items from destination */
        while (destination.Count > source.Count)
        {
            destination.RemoveAt(destination.Count - 1);
        }
    }

    return destination;
}


static void Main(string[] args)
{
    List<string> list1 = new List<string>();
    List<string> list2 = new List<string>();

    for (int i = 0; i < 100000; i++)
    {
        list1.Add(Guid.NewGuid().ToString());                
    }

    Stopwatch s = new Stopwatch();

    s.Start();
    CloneInto(list1, list2);

    s.Stop();

    Console.WriteLine("Your Method: " + s.Elapsed.Ticks);

    s.Reset();
    s.Start();

    list2 = list1.ToList();

    s.Stop();

    Console.WriteLine("ToList() Method: " + s.Elapsed.Ticks);

    Console.ReadKey();

结果:

但是,如果仅与内存有关-您的方法比 .ToList() 更好,并且您无能为力来进一步提高性能。也许您可以使用并行循环,例如 parallel.for,但不确定。

【讨论】:

    【解决方案3】:

    你为什么不直接使用

    destination = source.ToList();
    

    ToList() 创建列表的副本,之前在目标中的所有内容将在分配后立即准备好进行垃圾回收。

    【讨论】:

    • 阅读问题,他想将收藏移至现有收藏。
    • LInsoDeTeh,是的,功能上达到了目的,但是会导致额外的GC工作,并且在destination.Capacity >= source.Capacity的情况下,会导致不必要的内存分配。不是最佳的性能和内存使用。
    • 我已经阅读了这个问题。它会这样做。在代码之后,destination 将具有与 source 完全相同的条目,与您的代码具有相同的结果。
    • @BrendanHill - 对于中等大小的列表,GC 将有 less 以这种方式工作。
    • @LInsoDeTeh 在这行代码之后,destination 有一个新的引用,指向一个新的对象,一旦函数返回,该引用就会消失,稍后会收集垃圾。用于传递原始引用的变量(在外部函数调用中)仍然具有对原始未修改列表的引用。你的答案是错误的。
    猜你喜欢
    • 1970-01-01
    • 2021-11-01
    • 2022-11-16
    • 1970-01-01
    • 1970-01-01
    • 2017-02-03
    • 2011-08-22
    • 2012-03-05
    • 2014-09-11
    相关资源
    最近更新 更多