C# 中一个小类的内存优化

3

我有一段代码,可以将一个大型字符串矩阵转换为一个大型MyClass矩阵。 MyClass是我编写的一个小类,用于存储有关每个字符串的一些信息,其结构如下:

class MyClass
{
    public MyEnum Class { get; private set; }

    public int A { get; private set; }
    public int B { get; private set; }
    public int C { get; private set; }
    public int D { get; private set; }
}

目前该软件能够处理的矩阵大小为5-20列,100万行,但我希望将行数增加到接近1000万。我认为无法减少字符串矩阵的占用空间,但我希望减小MyClass的内存占用。
我可以使用short作为列A的类型,BCD的类型则使用byte,但这需要对代码进行大量重构。
我的问题是:
  1. 值得重构代码以使用shortbyte吗?
  2. 是否应将MyEnum重构为byte类型?
  3. 还有其他什么可以使类更高效的方法吗?
非常感谢您的时间!
编辑:更多背景信息- MyClass的矩阵是从字符串矩阵创建出来的,目的是进行分析。字符串矩阵从一个文本文件中读取,网络连接一般,因此将任务拆分成较小的块并不理想。

3
您真的需要同时将所有这些数据存储在内存中吗? - Daniel Renshaw
我必须承认,我没有认真考虑将任务分解的选项@DanielRenshaw - 感谢您让我更加深入地思考!事实上,对于我的问题来说并不理想。 - Root_Kabal
只是一个随意的、创新的想法,但你可以使用LogParser来完成你想要实现的任何目标吗? - Greg
考虑将其定义为结构体而非类。 - Brian Rasmussen
LogParser并不完全符合我的需求 - 这个工具必须能够处理各种不寻常的结构化文本格式,而且在没有文件模式的先前知识的情况下即时处理。 - Root_Kabal
5个回答

5
目前假设您的所有属性都是由实例变量实现的,并且在64位机器上运行,MyClass的实例是4B*4 + 8B = 24B。此外,由于您正在使用类(引用类型),MyClass的矩阵每个单元格还会增加8B的重量。这意味着您每个单元格使用32B。因此,一个10Mx20的MyClass矩阵使用约6.4GB的空间(对于这些大小,必须使用64位二进制文件),实际可能更多,因为我忽略了内存对齐要求。
如果您从类切换到结构体(值类型),则矩阵将直接存储MyClass实例,而不是指向MyClass实例的指针。因此,您将节省每个实例8B的空间。现在,内存使用量降至4.8GB。
如果您进一步调整实例变量,使用1个short和3个字节,并将枚举转换为byte,则每个实例仅使用6B。因此,总内存使用量将降至1.2GB。
无论如何,它实际上会更多,因为托管环境在每个对象中存储其他元数据,并且由于内存对齐需要为更快的访问时间填充对象。
PS:您实际上不需要更改属性的返回类型。您可以封装类型更改,并在MyClass的实现中执行转换,例如:
struct MyClass
{
  private short a; //Also consider ushort, if you need it
  //...

  public int A
  {
    get { return a; //Automatic promotion }
    private set
    {
      a = (short) value;
      System.Diagnostics.Debug.Assert(a == value, "Integer overflow");
    }
  }

  //...
}

这样,优化对使用MyClass的代码来说将是透明的。

2
请记住,单个对象的2GB限制。改为使用“struct”会导致一个大小为4.8 GB的数组,在.NET 4.5之前无法表示为单个对象,并且需要启用gcAllowVeryLargeObjects - Jim Mischel
我真的很喜欢这里的方法,因为它让我避免了重构代码库的其他方面,但考虑到我们正在处理数百万个操作,转换是否会带来性能损失? - Root_Kabal
@Root_Kabal 类型转换在大多数架构上都是单个机器指令,ALU总是以32位或64位精度工作,因此它们无论如何都会被转换为int。大部分开销将归因于需要更复杂的加载和存储指令来实际处理MyClass对象,而不是类型转换。 - Giulio Franco
1
@Root_Kabal:如果您像我展示的那样使用只读字段,就不需要setter,因此也不需要转换。而将short自动提升为int是没有成本的。 - Jim Mischel
谢谢@JimMischel,我没有注意到你提到的只读字段 - 我来自Java背景,不知道有这样一个有用的东西存在!事实证明,使用您或Giulio的方法,性能影响都是不存在的。 - Root_Kabal

2

有很多方法可以减小字符串矩阵的大小,当然这取决于字符串包含的内容。如果有许多重复的字符串,可以使用字符串池构建字符串池

如果您的字符串不是重复的,但通常是ASCII或其他单字节编码(或UTF-8中大部分是单字节字符),则可以通过构建字符串资源表来节省大量内存。请参见减少字符串所需的内存进行介绍。

对于您的`MyClass`,您支付每个实例分配开销16字节,几乎与数据本身占用的空间一样多。如果成员都是不可变的,我建议将其制作为`struct`。它们似乎是公开不可变的。您私下做什么,我不知道。但是可以尝试这样做:
[StructLayout(LayoutKind.Sequential, Pack=1)]
struct MyStruct
{
    public readonly MyEnum Class;
    public readonly int A;
    public readonly int B;
    public readonly int C;
    public readonly int D;

    public MyStruct(MyEnum cls, int a, int b, int c, int d)
    {
        Class = cls;
        A = a;
        B = b;
        C = c;
        D = d;
    }
}

每个实例的结果总共为20个字节,没有每个实例的分配开销。因此,您的10百万行乘以20列将是(10M * 20 * 20),约为4千兆字节。在.NET 4.5中,您可以使用gcAllowVeryLargeObjects配置设置来创建这么大的数组。
但要注意,您可能会遇到性能问题。请考虑以下代码:
MyStruct m = MyArray[x,y];
// now access fields of m

使用结构体,会在MyArray[x,y]处进行一次复制。这意味着复制了20个字节。同时,如果您修改了m.A,这个更改不会反映在数组中。您需要将其复制回去(例如,MyArray[x,y] = m;),或者完全放弃中间变量,直接写MyArray[x,y].A = 5;
当然,如果您的结构是不可变的,就没有复制回去的问题。
使用C#可以处理大量的内存数据,但必须创造性地处理。我发现以这种方式使用结构体非常有效,特别是如果它们是不可变的。

对我来说,有很多有用的字符串优化阅读材料-谢谢!乍一看,我担心对于这个特定问题,它们不会有很大的影响,因为问题的输入文件将具有极高的重复率,并且可以采用任何数量的编码。MyClass确实是不可变的,因此是成为结构体的好选择,除了建议的其他更改,应该会使我离目标更近。 - Root_Kabal
1
@Root_Kabal:仔细查看你的字符串编码。如果它们主要是单字节编码(或者 UTF-8 中大部分字符都是单字节字符),那么字符串资源表仍然可以为你节省很多空间。 - Jim Mischel
很遗憾,我必须为矩阵的大小设置一个定义好的限制,以覆盖软件可能遇到的任何类型的字符串。虽然大多数情况下它将是ASCII,但肯定会有需要处理UTF16字符串的情况 - 这意味着我的限制必须适应最坏的情况。 - Root_Kabal

1

short 是 16 位

int 是 32 位

你可以像 这里 描述的那样轻松设置枚举的大小,无需实际工作。

enum Days : byte {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};

这意味着你可以将班级规模减少一半。如果这对工作量来说足够好,那就取决于你自己。


在这种情况下,枚举的存储空间会被填充,所以可能并不重要。 - Brian Rasmussen

0

就让我们来谈一下如何使您的类更小,您使用其他数据类型的假设是正确的。这将减少所分配的整个内存量。至于内存中的数据展示,您似乎创建了一种hana(可能指HANA数据库),是吗?如果是这样的话,还有其他优化方法,主要是引用集合,也就是说,您不是在每列中存储实际值,而是存储对属于独特条目的其他值的引用字典。此外,您还必须以另一种方式对齐您的数据。不要考虑行导向,而是改为考虑列导向的内存数据展示方式(或者至少在您的脑海中如此)。

这些都是SAP HANA用来在内存中存储大量数据而不是硬盘上的技术。


0
如果您使用类,可以通过创建n个MyClass子类之一来获得收益,每个子类对应于MyEnum的一个值(如果MyEnum具有离散数量的值),然后删除MyEnum
这仅在MyClass是一个明确的类时才有效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接