有效地存储数据以节省内存使用量,而无需 C# 中的查找开销
Posted
技术标签:
【中文标题】有效地存储数据以节省内存使用量,而无需 C# 中的查找开销【英文标题】:Storing data efficiently to save ram usage, without lookup overhead in C# 【发布时间】:2019-02-08 08:53:15 【问题描述】:根据我的代理所处的位置以及它的旋转,我需要确定到墙壁的距离。由于这个函数需要一点点并且需要大量调用,我的想法是通过离散化位置 x 和 y 以及角度来存储距离。
因此,我的函数调用如下:
float GetWallDistance(int x, int y, int angle)
return CalcWallDistance(x, y, angle);
其中x和y的范围是0到500,角度范围是0到360。我的第一个想法是把它存储在一个多维数组中,如下所示:
使用float[,,] saveArray = new float[500, 500, 360];
在某处初始化数组
float GetWallDistance(int x, int y, int angle)
float distance = 0;
if(saveArray[x, y, angle] == 0)
distance = CalcWallDistance(x, y, angle);
saveArray[x, y, angle] = distance;
else
distance = saveArray[x, y, angle];
return distance;
这极大地加快了计算时间,但这里的问题是 saveArray 占用了大量内存,并且代理很可能不会遍历整个 500 x 500 x 360 的搜索空间。因此,大量内存被白白占用。
因此,我使用字典来更高效地存储数据,如下所示:
使用Dictionary<double, float> saveDictionairy = new Dictionary<double, float>();
在某处初始化字典
float GetWallDistance(int x, int y, int angle)
double key = (double)x * 1000 + (double)y + (double)angle/1000
float distance = 0;
if(!saveDictionairy.TryGetValue(key, out distance))
distance = CalcWallDistance(x, y, angle);
saveDictionairy.Add(key, distance);
return distance;
(我尝试使用字符串作为字典的键,但似乎连接 x、y 和角度显然需要相当长的时间)
这种方法确实更节省内存,但是相对于索引多维数组,使用键项查找字典的时间增加了很多。
有谁知道如何以一种易于查找的方式有效地存储这些数据?
【问题讨论】:
我不确定你在计算什么。但是您似乎可以使用System.Numeric.Vector2
(或3)。
Dictionary<K,V>
在内部使用数组,所以我认为它使用的内存至少与float[,,]
一样多(可能会更多,因为它会引入一点开销)。
你研究过稀疏数组数据结构吗?
@ThomasFlinkow 不同之处在于字典的内部数组将只包含代理访问过的元素,而不是整个 500x500x360 搜索空间中的所有可能元素。
@canton7 哦,你是对的,我只考虑了每个元素都被访问过的(罕见的)情况。谢谢。
【参考方案1】:
.NET Dictionary
使用快速算法,但仍然有相当高的开销。不久前我尝试过让它更快。我发现通过删除我不需要的东西并进行其他设计更改,我可以将速度提高 6 倍。
例如,Dictionary
使用模运算符从哈希码映射到存储桶。 %
出奇的慢。我认为它需要 31 个 CPU 周期。当我用 hashCode & bucketCountMask
替换它时,其中桶数是 2 的幂,bucketCountMask
是 buckets.Length - 1
,我立即意识到性能有了很大的提升。
我还删除了对删除项目和迭代器 version
功能的支持。我直接暴露了slots
数组,这样调用者就可以直接改变其中的数据。
这种自定义类型要快得多,因为它更专业地满足我的需求,而且它的 API 更难使用。
GitHub 上的 .NET 代码包含一个 DictionarySlim
类型供其内部使用。也许你可以使用它。
【讨论】:
感谢您的回复。为了让事情更上一层楼,代码字典也由不同的线程并行访问。因为它是一个统一项目,所以我发现了这个统一的并发词典link。你能帮我加快这本词典的速度吗? 并发字典往往有更多的开销。在这里,您可以通过以下方案在很大程度上避免这种开销。每个线程都有一个本地非同步字典。写总是在那里。读取测试全局和本地字典。您有时会暂停所有线程并将本地字典合并到全局字典中。清除本地的。您可以在游戏模拟中每帧执行一次。 或者,您可以使用该线程安全实现,但修改代码,以便您首先尝试以完全不同步的方式阅读。如果“破解”读取成功,您将获得非常快速的读取。如果失败,您必须走缓慢但安全的读取路径并重试。这是有效的,因为您的条目是不可变的。一旦写入,值就永远不会改变,也不会消失。【参考方案2】:从您当前列出的选项来看,矩阵方法似乎是您在性能和内存分配方面的最佳选择。
我已经运行了以下基准测试:
[Benchmark(Baseline = true)]
public void MatrixTest()
// float[,,] saveArray = new float[501, 501, 361];
for (int x = 0; x <= 500; x++)
for (int y = 0; y <= 500; y++)
for (int angle = 0; angle <= 360; angle++)
if (saveArray[x, y, angle] == 0) saveArray[x, y, angle] = 42;
[Benchmark]
void IntKeyDictionaryTest()
// Dictionary<int, float> saveDictionary = new Dictionary<int, float>();
for (int x = 0; x <= 500; x++)
for (int y = 0; y <= 500; y++)
for (int angle = 0; angle <= 360; angle++)
int key = (x << 18) | (y << 9) | (angle);
if (!saveDictionary.TryGetValue(key, out float d)) saveDictionary[key] = 42;
[Benchmark]
void DoubleKeyDictionaryTest()
// Dictionary<double, float> saveDictionary = new Dictionary<double, float>();
for (int x = 0; x <= 500; x++)
for (int y = 0; y <= 500; y++)
for (int angle = 0; angle <= 360; angle++)
double key = (double)x * 1000 + (double)y + (double)angle / 1000l;
if (!saveDictionary.TryGetValue(key, out float d)) saveDictionary[key] = 42;
结果如下:
Method | Mean | Error | StdDev | Ratio | RatiosD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
------------------------ |------------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:|
MatrixTest | 727.9 ms | 5.733 ms | 5.363 ms | 1.00 | 0.00 | - | - | - | - |
IntKeyDictionaryTest | 4,682.1 ms | 12.017 ms | 11.241 ms | 6.43 | 0.05 | - | - | - | - |
DoubleKeyDictionaryTest | 12,804.1 ms | 66.425 ms | 62.134 ms | 17.59 | 0.17 | - | - | - | - |
所以我设法为您的字典制作了一个更有效的键,因为我知道 x、y 和角度每个都可以表示为 9 位 => 总共 27 位,适合 int。 无论如何,从外观上看,矩阵方法似乎是赢家。
【讨论】:
这确实是我在运行代码时注意到的。问题仍然存在,矩阵在分配时占用了大量内存,而它的许多条目将不会被使用。我想最好的方法是像 Canton7 建议的那样,使用轨道作为参考系统,矩阵的输入为:(沿轨道的离散距离,到轨道中间的离散距离,离散角度)。这将在保持快速查找速度的同时减小矩阵的大小。以上是关于有效地存储数据以节省内存使用量,而无需 C# 中的查找开销的主要内容,如果未能解决你的问题,请参考以下文章
以节省内存的方式从 python 中的流创建 Parquet 文件
是否可以将 C# double[,,] 数组转换为 double[] 而无需复制