有效地存储数据以节省内存使用量,而无需 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 的幂,bucketCountMaskbuckets.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# 中有效地从 SQL 数据读取器写入文件?

C#基础按值和按引用传递参数

是否可以将 C# double[,,] 数组转换为 double[] 而无需复制

如何有效地乘以重复行的火炬张量而不将所有行存储在内存中或迭代?

在c#中使用字符数组(char [])而不是字符串