我需要非常频繁地对一个相当大的集合(高数百/数千个项目)进行排序,即每帧60 fps(我正在使用Unity)。 计算每个项目的密钥有点慢,因此需要进行缓存。
我尝试了各种方法:
这是一种耻辱,因为SortedList似乎非常适合这项工作,是否有任何我无法使用GC的替代方案?
答案 0 :(得分:1)
如果计算键太慢,您可以将key
属性添加到商品类中,在排序前计算它,然后使用第一种方法IComparer
简单地比较键。
答案 1 :(得分:0)
简答:您可以使用带有 List.Sort
的 static 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;
}
}
}
}