这是否适合类或结构(速度比内存更重要)?

时间:2012-08-24 22:28:11

标签: c# .net class struct

通常情况下,我永远不必问自己一个特定场景是否更适合于一个结构或类,坦率地说,在这种情况下我没有问过这个问题。现在我正在优化,事情变得有些混乱。

我正在编写一个数字运算应用程序,处理包含数百万Base10数字的极大数字。数字是2D空间中的(x,y)坐标。主算法非常顺序,并且在任何给定时间内存不超过200个类Cell(下面列出)的实例。该类的每个实例占用大约5MB的内存,导致应用程序的总峰值内存不超过1GB。成品将在16核机器上运行,内存为20GB,没有其他应用程序占用资源。

这是班级:

// Inheritance is convenient but not absolutely necessary here.
public sealed class Cell: CellBase
{
    // Will contain numbers with millions of digits (512KB on average).
    public System.Numerics.BigInteger X = 0;
    // Will contain numbers with millions of digits (512KB on average).
    public System.Numerics.BigInteger Y = 0;

    public double XLogD = 0D;
    // Size of the array is roughly Base2Log(this.X).
    public byte [] XBytes = null;

    public double YLogD = 0D;
    // Size of the array is roughly Base2Log(this.Y).
    public byte [] YBytes = null;

    // Tons of other properties for scientific calculations on X and Y.
    // NOTE: 90% of the other fields and properties are structs (similar to BigInteger).

    public Cell (System.Numerics.BigInteger x, System.Numerics.BigInteger y)
    {
        this.X = x;
        this.XLogD = System.Numerics.BigInteger.Log(x, 2);
        this.XBytes = x.ToByteArray();

        this.Y = y;
        this.YLogD = System.Numerics.BigInteger.Log(y, 2);
        this.YBytes = y.ToByteArray();
    }
}

我选择使用类而不是结构只是因为它“感觉”更自然。字段,方法和内存的数量本能地指向类而不是结构。我进一步证明,通过考虑临时赋值调用会有多少开销,因为底层主要对象是BigInteger的实例,BigInteger本身就是一个结构。

问题是,我在这里明智地选择了速度效率是这种情况下的最终目标吗?

这里有一些关于算法的信息,以防万一。在每次迭代中:

  1. 在所有200个实例上执行一次排序。执行时间的20%。
  2. 计算感兴趣的相邻(x,y)坐标。 60%的执行时间。
  3. 上面第2点的并行/线程开销。执行时间的10%。
  4. 分支开销。执行时间的10%。
  5. 最昂贵的功能:BigInteger.ToByteArray()(implementation)

5 个答案:

答案 0 :(得分:4)

由于种种原因,包括

在内,这更适合作为一个班级
  • 它在逻辑上不代表单个值
  • 大于16字节
  • 这是可变的

有关详细信息,请参阅Choosing Between Classes and Structures

此外,我还建议它更适合给定的课程:

  • 它包含引用类型(数组)。包含类的结构很少是一个好的设计理念。

但是,考虑到你正在做的事情,尤其如此。如果您使用struct,则排序将需要整个结构的副本,而不仅仅是引用的副本。方法调用(除非由ref传递)也会产生巨大的开销,因为你要复制所有数据。

集合中项目的并行化也可能产生巨大的开销,因为检查结构的任何数组的边界(即:如果它保存在List<Cell>或类似)会导致错误的错误共享,因为所有访问列表将访问列表开头的内存。

我建议将其作为一个类,并且,此外,我建议尝试将字段移动到属性中,并使类尽可能不可变。这将有助于保持您的设计清洁,并且在多线程时不太可能出现问题。

答案 1 :(得分:2)

很难根据你写的内容来判断(例如我们不知道你最终复制Cell类型的值的频率),但我会强烈期待class这里是正确的方法。

类中方法的数量无关紧要,但是如果它有很多字段,则需要考虑在将值传递给另一个方法时复制所有这些字段的影响(等)

从根本上说它并不像开始时那样感觉 - 但我明白,如果表现特别重要,那么哲学方面对你来说可能并不那么有趣。

所以是的,我认为你做出了正确的决定,我认为目前没有理由相信任何其他事情 - 当然,如果你能轻易地改变决定并且测试它就像一个结构,比猜测更好。表现非常难以准确预测。

答案 2 :(得分:1)

由于您的类确实包含占用大部分内存的数组,并且您只有200个单元实例,因此本身的内存消耗不是问题。你认为一堂课更自然是对的,这确实是正确的选择。我的猜测是XByte []和XYBytes []的比较会限制你的排序时间。这一切都取决于您的阵列有多大以及如何进行比较。

答案 3 :(得分:1)

让我们开始忽视性能问题,并为他们做好准备。

结构是ValueTypes,ValueTypes是值类型。整数和DateTime是值类型和良好的比较。谈论一个1是如何与1相同或一个2010-02-03T12:45:23.321Z是如何相同或不相同时,没有任何意义作为另一个2010-02-03T12:45:23.321Z。它们在不同的用途中可能有不同的意义,但是1 == 1和1!= 2和2010-02-03T12:45:23.321Z == 2010-02-03T12:45:23.321Z和2010-02-03T12 :45:23.321Z!= 2931-03-05T09:21:29.43Z是整数和日期时间的固有特性,也是使它们成为价值类型的原因。

这是最纯粹的思考方式。如果它与上面匹配,则它是一个值类型,如果它不是,它就是一个引用类型。没有其他东西进入它。

扩展1:如果X可以有X,那么它必须是引用类型。这在逻辑上是否遵循上述内容是值得商榷的,但无论你如何看待这个问题,你都可以拥有一个结构,在实践中有一个另一个实例作为成员(直接或间接),以便&那是。

扩展2:有人说,来自可变结构的困难来自上述,有些则没有。但是,无论你怎么想这件事,都存在实际困难。在少数情况下,可变结构可能很有用,但是它们引起了足够的混淆,以至于它们应该被限制为私有情况作为优化而不是公共情况。

这是绩效位......

值类型和引用类型在不同的情况下具有不同的特性,这些特性影响速度,内存使用以及内存使用以多种方式影响垃圾收集的方式,从而在性能方面给出每种不同的优缺点。我们对此付出了多少关注,取决于我们需要多少才能达到这个水平。现在值得一提的是,如果你遵循上面关于结构和阶级之间决定的规则,它们的不同方式往往会取得平衡,所以如果我们开始考虑这个问题,我们就会在最不接近优化领域。

优化等级1.

如果值类型实例每个实例包含超过16个字节,则应该将其作为引用。有时甚至将其称为“天然的”#34;差异而不是优化之一。严格来说,&#34;值类型&#34;中没有任何内容。这需要&#34; 16或更少的字节&#34;但它确实倾向于平衡那种方式。

远离简单的&#34; 16字节&#34;规则越小,复制的速度就越快,反之亦然,因此对于一个20字节的实例,将其弯曲的影响小于为200字节实例弯曲它的影响。

你需要打包和拆箱吗?自从引入泛型以来,我们已经能够避免很多情况下我们打算使用1.0和1.1打包和取消打包,所以这并不像以前那么大,但是如果你这样做就会受到伤害性能

优化等级2.

值类型可以放在堆栈上,直接放在数组中(而不是对它们的引用)并且是结构或类的直接字段(再次,而不是对它们的引用)可以访问它们和他们的领域更快。

如果您要创建一个数组,如果全零值对您来说是一个有用的起点,那么您可以立即得到它,与参考类型相同空数组。这可以使结构更快。

编辑:从上面扩展的内容,如果您要快速迭代数组,那么直接访问可以提升跟随引用,您可以&#39; ll一次将几个实例加载到CPU缓存中(当前x86-32或x86-64 / amd值为64字节,ia-64值为128字节)。它必须是一个非常紧密的循环,但有些情况确实如此。

几乎是最多&#34;我选择结构而不是课堂以获得表现&#34;归结为第一点,或第一点与第二点结合。

优化等级3.

如果您遇到的情况是您所关注的某些值是彼此重复的,并且它们的大小很大,那么使用不可变实例(或者可变实例,一旦开始执行以下操作,您就永远不会变异),你可以故意别名不同的引用,以便节省大量的内存,因为你的例如20个重复的2kiB对象实际上是同一个对象,因此在这种情况下可以节省26kiB。它还可以更快地进行比较,因为您可以更频繁地查看身份。这只能通过引用类型来完成。

优化等级4.

具有数组的结构虽然对包含的数组进行别名,但可以在内部使用上述技术,平衡这一点,尽管它涉及的程度更高。

优化等级X。

如果实际测量的结果是不同的,那么对这些优缺点的思考并不重要。既然有利有弊,那么总是有可能弄错。

在思考1到4时,除了这些优化问题之外,还有值和参考类型之间的差异,我认为你应该去上课。

在考虑X级时,如果你的实际测试证明我错了,我不会感到惊讶。最好的一点是,如果从类更改为struct(你大量使用别名或null值的可能性)是艰难的,那么你可以非常自信这样做是一种失败。如果它不是那么艰难,那么你可以这样做并测量!我强烈建议测量一项测试,该测试涉及实际操作10000次以上 - 如果您在真实情况下执行不同的操作20次,那么您是否可以在几秒钟内完成10,000次特定操作事?

答案 4 :(得分:0)

如果(1)结构的状态取决于数组的标识而不是其内容(如ArraySegment的情况),结构只能安全地包含数组类型字段,或者( 2)任何可能试图改变它的东西都不会引用数组(通常,这意味着数组字段将是私有的,结构本身将创建数组并执行将要执行的所有修改它,在字段中存储引用之前)。

我主张比其他人更常使用结构,但是你的数据存储内容有两个数组类型的字段似乎是反对使用结构的强烈论据。