3

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

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

这是课程:

// 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 的实例,它本身就是一个结构。

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

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

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

5 回答 5

4

由于许多原因,这将更适合作为一个班级,包括

  • 它在逻辑上不代表单个值
  • 它大于 16 个字节
  • 它是可变的

有关详细信息,请参阅在类和结构之间进行选择

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

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

但是,鉴于您正在做的事情,这一点尤其正确。如果要使用 a struct,排序将需要整个结构的副本,而不仅仅是引用的副本。方法调用(除非通过 ref 传递)也会产生巨大的开销,因为您将复制所有数据。

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

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

于 2012-08-24T22:34:30.140 回答
2

根据您编写的内容很难判断(Cell例如,我们不知道您最终复制类型值的频率),但我强烈希望 aclass在这里是正确的方法。

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

从根本上说,它不像一种价值类型——但我知道,如果性能特别重要,那么哲学方面对你来说可能就不那么有趣了。

所以是的,我认为你做出了正确的决定,我认为目前没有理由相信其他任何事情 - 但当然,如果你可以轻松地改变决定并将其作为结构进行测试,那将比猜测要好。性能非常难以准确预测。

于 2012-08-24T22:34:49.313 回答
1

由于您的类确实包含消耗大部分内存的数组,并且您只有 200 个单元实例围绕类本身的内存消耗不是问题。你说得对,一门课感觉更自然,这确实是正确的选择。我的猜测是 XByte[] 和 XYBytes[] 的比较确实限制了您的排序时间。这完全取决于您的数组有多大以及您如何执行比较。

于 2012-08-24T22:34:07.807 回答
1

让我们开始忽略性能问题,并努力解决它们。

结构是值类型,值类型是值类型。Integer's 和DateTime's 是值类型和很好的比较。谈论一个人与另一个人的相同或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:有人说可变结构带来的困难来自上述,而有些人则不然。不过,无论您对此事有何看法,都存在实际困难。可变结构在少数情况下可能很有用,但它们会引起足够的混乱,以至于它们应该被限制在私有情况下作为优化,而不是作为理所当然的公共情况。

性能位来了...

值类型和引用类型在不同情况下具有不同的特性,它们会影响速度、内存使用以及内存使用对垃圾收集的影响方式,这些方式在性能方面各有不同的优缺点。我们对此付出了多少关注,取决于我们需要多少才能达到那个水平。现在值得说的是,如果您遵循上述关于在 struct 和 class 之间做出决定的规则,那么它们的不同方式往往会取得平衡,因此如果我们开始考虑这一点,那么我们至少接近于优化领域。

优化级别 1。

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

远离简单的“16 字节”规则,它越小复制速度越快,反之亦然,因此为 20 字节实例弯曲它比为 200 字节实例弯曲它的影响更小。

您需要大量装箱和拆箱吗?自从引入泛型以来,我们已经能够避免很多使用 1.0 和 1.1 装箱和拆箱的情况,所以这不像以前那么重要,但如果这样做会损害性能。

优化级别 2。

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

如果您要创建它们的数组,并且如果全零值对您来说是一个有用的起点,那么您会立即得到它,而对于引用类型,您会得到一个空数组。这可以使结构更快。

编辑:从上面延伸出来的东西,如果你要快速迭代数组,那么除了直接访问提供了比参考更重要的提升之外,你将加载几个实例到 CPU 缓存中时间(当前 x86-32 或 x86-64/amd 上 64 字节,ia-64 上 128 字节)。它必须是一个非常紧密的循环,但在某些情况下确实如此。

几乎大多数“我选择 struct 而不是 class 以获得性能”归结为第一点,或者第一点与第二点相结合。

优化级别 3。

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

优化级别 4。

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

优化级别 X。

如果实际测量结果会得出不同的结果,那么对这些利弊的思考多少会得出一个特定的答案,这并不重要。由于有利有弊,因此总是有可能出错。

在考虑 1 到 4 时,除了这些优化问题之外,除了值类型和引用类型之间的差异之外,我认为您应该选择一个类。

如果您的实际测试证明我错了,那么在考虑 XI 级时不会感到惊讶。最好的一点是,如果从 class 更改为 struct 很困难(您大量使用别名或 null 值的可能性),那么您可以确信这样做是失败的。如果它不费力,那么你可以这样做并测量!我强烈建议测量一个涉及实际运行 10,000 次的测试 - 如果您在实际中多执行 20 次不同的操作,谁会在乎您是否可以在几秒钟内执行给定的操作 10,000 次?

于 2012-08-24T23:29:01.617 回答
0

一个结构只能在以下情况下安全地包含一个数组类型字段:(1)结构的状态取决于数组的标识而不是它的内容(就像 的情况一样ArraySegment),或者(2)没有对数组的引用永远被任何可能试图改变它的东西持有(通常,这意味着数组字段将是私有的,并且结构本身将创建数组并执行将对其进行的所有修改,然后将引用存储在场地)。

我提倡比这里的其他人更普遍地使用结构,但是您的数据存储事物将具有两个数组类型字段这一事实似乎是反对使用结构的有力论据。

于 2012-08-25T22:34:46.513 回答