有没有办法在不分配任何内存的情况下对数组进行排序?

时间:2014-09-08 17:40:26

标签: c# unity3d

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

我尝试了各种方法:

  • 使用IComparer的List.Sort(),每次计算密钥:超慢
  • SortedList:更快,但生成GC分配(30KB /帧):为什么?是他们的钥匙盒装(我用了很长时间)?是分配的键/值对?如果我将一个long包装在一个类中,那么GC会减半,所以我的猜测是“两个”:1对分配,如果它是一个值类型,则为分配一个键...
  • Array.Sort(keyArray,valueArray):可怕!慢慢的生成256KB的GC /帧!

这是一种耻辱,因为SortedList似乎非常适合这项工作,是否有任何我无法使用GC的替代方案?

2 个答案:

答案 0 :(得分:1)

如果计算键太慢,您可以将key属性添加到商品类中,在排序前计算它,然后使用第一种方法IComparer简单地比较键。

答案 1 :(得分:0)

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

避免使用 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;
        }
    }
}

}