【问题标题】:Is there a way to sort arrays without allocating any memory?有没有办法在不分配任何内存的情况下对数组进行排序?
【发布时间】:2014-09-08 17:40:25
【问题描述】:

我需要非常频繁地对一个相当大的集合(高数百/低数千个项目)进行排序,即每帧 60 fps(我正在使用 Unity)。 计算每个项目的键有点慢,所以需要缓存。

我尝试了各种方法:

  • List.Sort() 和 IComparer,每次都计算 key:超级慢
  • SortedList:快得多,但生成 GC 分配(30KB/帧):为什么?他们是盒装的(我用的是长的)吗?是否分配了键/值对?如果我将 long 包装在一个类中,则 GC 减半,所以我的猜测是“两者”:1 个分配用于该对,一个分配用于将键装箱(如果它是值类型)...
  • Array.Sort(keyArray, valueArray):太可怕了!速度慢,每帧产生 256KB 的 GC!

很遗憾,因为 SortedList 似乎非常适合这项工作,是否有任何我缺少的无 GC 替代方案?

【问题讨论】:

  • 在每一帧上都至关重要吗?也许您可以使用coroutines 跨多个帧计算它
  • 任何内存,没有。您至少需要一个交换变量来执行任何类型的排序。使用的内存取决于算法,插入或选择排序几乎不占用内存,快速排序更快,而且占用的内存也很少。你可以自己实现一个,看看它对任何 .NET 使用的效果如何!
  • This 可以帮助您入门。
  • 尝试使用SortedDictionary,内存占用会更小。在您的情况下,它也可能更快。
  • 您是否考虑过使用SortedSet<T> 而不是List&lt;T&gt; 来管理您的项目?这将使项目保持排序顺序,因此不需要排序。

标签: c# unity3d


【解决方案1】:

如果计算键太慢,您可以将key 属性添加到您的项目类,在排序之前计算它,然后使用您的第一个方法与IComparer 简单地比较键。

【讨论】:

  • 是的,我尝试过这种方法,但问题是 List.Sort 本身很慢。
【解决方案2】:

简答:您可以使用List.Sortstatic Func&lt;&gt; 进行排序 没有每帧分配。它将首先分配少量 使用,但不会为每个后续排序分配更多。

避免使用 IComparer,因为它似乎会导致每帧分配。

要实现真正的零分配,您需要实现自己的排序 算法。但是,您需要确保您的排序比内置排序更快,而无需额外的内存使用。请注意,List.Sort's Remarks section 描述了它使用的三种可能的排序算法。如果没有执行所有的实施细节,我可能会遗漏一些东西。

这里有一些标记为Unity's Profiler 的代码以及我从分析器获得的读数。它使用@AntonSavin 的建议在排序前计算键,演示了 0 分配的插入排序,并比较了几种调用 List.Sort() 的方式:

using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System;
using UnityEngine;
using UnityEngine.Profiling;
using Random = UnityEngine.Random;

static class SortTest {

[UnityEditor.MenuItem("Tools/Sort numbers")]
static void Menu_SortNumbers()
{
    var data = new List<Data>(1000);
    for (int i = 0; i < 1000; ++i)
    {
        data.Add(new Data{ Value = Random.Range(0, 10000) });
    }

    for (int i = 0; i < data.Count; ++i)
    {
        data[i].ComputeExpensiveKey();
    }

    var to_sort = Enumerable.Range(0, 8)
        .Select(x => data.ToList())
        .ToList();

    // Focus is GC so we re-run the sort several times to validate
    // additional sorts don't have additional cost.
    const int num_iterations = 100;

    // GC Alloc: 0 B
    Profiler.BeginSample("Sort 1,000 items 1 time - Insertion"); {
        InsertionSort(to_sort[0], compare_bool);
    }
    Profiler.EndSample();

    // GC Alloc: 0 B
    // Time ms: 16.47
    Profiler.BeginSample("Sort 1,000 items 100 times - Insertion"); {
        for (int i = 0; i < num_iterations; ++i) {
            InsertionSort(to_sort[1], compare_bool);
        }
    }
    Profiler.EndSample();

    // GC Alloc: 48 B
    // Alloc is static -- first use is automatically cached.
    Profiler.BeginSample("Sort 1,000 items 1 time - List.Sort Comparison");
    {
        to_sort[2].Sort(compare_int);
    }
    Profiler.EndSample();

    // GC Alloc: 0 B (because of previous sample)
    // Time ms: 8.51
    Profiler.BeginSample("Sort 1,000 items 100 times - List.Sort Comparison"); {
        for (int i = 0; i < num_iterations; ++i) {
            to_sort[3].Sort(compare_int);
        }
    }
    Profiler.EndSample();

    // GC Alloc: 112 B
    Profiler.BeginSample("Sort 1,000 items 1 time - List.Sort lambda"); {
            to_sort[4].Sort((a,b) => {
                if (a.PrecomputedKey < b.PrecomputedKey)
                {
                    return -1;
                }
                if (a.PrecomputedKey > b.PrecomputedKey)
                {
                    return 1;
                }
                return 0;
            });
    }
    Profiler.EndSample();

    // GC Alloc: 112 B
    // Time ms: 8.75
    // Seems like this does a callsite caching (for some reason more than
    // Comparison). Each location that invokes Sort incurs this alloc.
    Profiler.BeginSample("Sort 1,000 items 100 times - List.Sort lambda"); {
        for (int i = 0; i < num_iterations; ++i) {
            to_sort[5].Sort((a,b) => {
                if (a.PrecomputedKey < b.PrecomputedKey)
                {
                    return -1;
                }
                if (a.PrecomputedKey > b.PrecomputedKey)
                {
                    return 1;
                }
                return 0;
            });
        }
    }
    Profiler.EndSample();

    // GC Alloc: 112 B
    Profiler.BeginSample("Sort 1,000 items 1 time - List.Sort IComparer"); {
            to_sort[6].Sort(compare_icomparer);
    }
    Profiler.EndSample();

    // GC Alloc: 10.9 KB (num_iterations * 112 B)
    // Time ms: 8.48
    Profiler.BeginSample("Sort 1,000 items 100 times - List.Sort IComparer"); {
        for (int i = 0; i < num_iterations; ++i) {
            to_sort[7].Sort(compare_icomparer);
        }
    }
    Profiler.EndSample();

    Profiler.enabled = false;


    // Make sure they sorted the same.
    for (int i = 0; i < to_sort[0].Count; ++i)
    {
        foreach (var list in to_sort)
        {
            UnityEngine.Assertions.Assert.AreEqual(to_sort[0][i].Value, list[i].Value);
        }
    }
    Debug.Log("Done SortNumbers");
}

class Data
{
    public int PrecomputedKey;
    public int Value;
    public void ComputeExpensiveKey()
    {
        // something expensive here
        PrecomputedKey = Value;
    }
}

// Create outside of your loop to reduce allocations.
static Func<Data,Data,bool> compare_bool = (a,b) => a.PrecomputedKey > b.PrecomputedKey;
static Comparison<Data> compare_int = (a,b) => {
    if (a.PrecomputedKey < b.PrecomputedKey)
    {
        return -1;
    }
    if (a.PrecomputedKey > b.PrecomputedKey)
    {
        return 1;
    }
    return 0;
};
static IComparer<Data> compare_icomparer = Comparer<Data>.Create((a,b) => {
    if (a.PrecomputedKey < b.PrecomputedKey)
    {
        return -1;
    }
    if (a.PrecomputedKey > b.PrecomputedKey)
    {
        return 1;
    }
    return 0;
});

static void InsertionSort<T>(List<T> items, Func<T,T,bool> compare)
{
    var len = items.Count;
    for (int i = 0; i < len; ++i)
    {
        var current = items[i];
        for (int j = i - 1; j >= 0 && !compare(current, items[j]); --j)
        {
            items[j+1] = items[j];
            items[j] = current;
        }
    }
}

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    • 1970-01-01
    • 2020-01-25
    • 1970-01-01
    • 2016-12-25
    • 1970-01-01
    • 2022-11-15
    相关资源
    最近更新 更多