35

我正在做一个光线追踪器爱好项目,最初我使用结构体作为我的 Vector 和 Ray 对象,我认为光线追踪器是使用它们的完美情况:你创建了数百万个它们,它们的寿命不会超过一个方法,它们是轻量级的。然而,通过简单地将 Vector 和 Ray 上的“struct”更改为“class”,我获得了非常显着的性能提升。

是什么赋予了?它们都很小(向量 3 个浮点数,射线 2 个向量),不要过度复制。当然,我确实会在需要时将它们传递给方法,但这是不可避免的。那么,在使用结构时会导致性能下降的常见陷阱有哪些?我读过这篇MSDN 文章,内容如下:

当您运行此示例时,您会发现结构循环的速度要快几个数量级。但是,当您将它们视为对象时,请注意使用 ValueTypes 是很重要的。这会为您的程序增加额外的装箱和拆箱开销,并且最终可能会比您卡在对象上的成本更高!要查看实际情况,请修改上面的代码以使用 foos 和 bar 数组。您会发现性能或多或少相等。

然而,它已经很老了(2001 年),整个“把它们放在一个数组中导致装箱/拆箱”让我觉得很奇怪。真的吗?但是,我确实预先计算了主光线并将它们放在一个数组中,所以我接受了这篇文章并在需要时计算了主光线,并且从未将它们添加到数组中,但它并没有改变任何东西:类,它仍然快 1.5 倍。

我正在运行 .NET 3.5 SP1,我相信它解决了 struct 方法从未内联的问题,因此也不可能是这样。

所以基本上:任何提示,要考虑的事情以及要避免什么?

编辑:正如一些答案中所建议的,我已经建立了一个测试项目,我尝试将结构作为参考传递。添加两个向量的方法:

public static VectorStruct Add(VectorStruct v1, VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static VectorStruct Add(ref VectorStruct v1, ref VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static void Add(ref VectorStruct v1, ref VectorStruct v2, out VectorStruct v3)
{
  v3 = new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

对于每个我都有以下基准方法的变体:

VectorStruct StructTest()
{
  Stopwatch sw = new Stopwatch();
  sw.Start();
  var v2 = new VectorStruct(0, 0, 0);
  for (int i = 0; i < 100000000; i++)
  {
    var v0 = new VectorStruct(i, i, i);
    var v1 = new VectorStruct(i, i, i);
    v2 = VectorStruct.Add(ref v0, ref v1);
  }
  sw.Stop();
  Console.WriteLine(sw.Elapsed.ToString());
  return v2; // To make sure v2 doesn't get optimized away because it's unused. 
}

一切似乎都表现得几乎相同。它们是否有可能被 JIT 优化为传递此结构的最佳方式?

EDIT2:我必须注意,在我的测试项目中使用结构使用类快大约 50%。为什么这对我的光线追踪器有所不同,我不知道。

4

12 回答 12

29

结构数组将是内存中的单个连续结构,而对象数组(引用类型的实例)中的项需要通过指针单独寻址(即对垃圾收集堆上对象的引用)。因此,如果您一次处理大量项目集合,结构将给您带来性能提升,因为它们需要更少的间接。此外,结构不能被继承,这可能允许编译器进行额外的优化(但这只是一种可能性,取决于编译器)。

但是,结构具有完全不同的赋值语义,也不能被继承。因此,除了给定的性能原因,我通常会在需要时避免使用结构。


结构

由结构(值类型)编码的值数组 v 在内存中如下所示:

vvvv

班级

由类(引用类型)编码的值数组 v 如下所示:

公私合营

..v..v...vv。

其中 p 是 this 指针或引用,它指向堆上的实际值 v。这些点表示可能散布在堆上的其他对象。在引用类型的情况下,您需要通过相应的 p 来引用 v,在值类型的情况下,您可以直接通过其在数组中的偏移量来获取值。

于 2009-02-28T15:46:17.000 回答
12

在何时使用结构的建议中,它说它不应大于 16 字节。您的 Vector 是 12 个字节,接近限制。Ray 有两个 Vector,将其设置为 24 字节,这显然超过了建议的限制。

当一个结构大于 16 字节时,它不能再用一组指令有效地复制,而是使用循环。所以,通过这个“神奇”的限制,当你传递一个结构时,实际上你做的工作比传递一个对象的引用要多得多。这就是为什么即使在分配对象时开销更大,使用类的代码也更快的原因。

Vector 仍然可以是一个结构,但是 Ray 太大而不能作为一个结构很好地工作。

于 2009-02-28T04:04:24.410 回答
9

在 .NET 泛型之前写的任何关于装箱/拆箱的东西都可以用一粒盐来对待。通用集合类型不再需要对值类型进行装箱和拆箱,这使得在这些情况下使用结构更有价值。

至于您的具体减速 - 我们可能需要查看一些代码。

于 2009-02-28T01:12:22.953 回答
7

基本上,不要让它们太大,并尽可能通过 ref 传递它们。我以完全相同的方式发现了这一点......通过将我的 Vector 和 Ray 类更改为结构。

随着更多的内存被传递,它必然会导致缓存抖动。

于 2009-02-28T01:12:51.733 回答
7

我认为关键在于您帖子中的这两个陈述:

你创造了数百万个

当然,我确实在需要时将它们传递给方法

现在,除非您的结构的大小小于或等于 4 个字节(如果您在 64 位系统上,则为 8 个字节),如果您只是传递一个对象引用,那么您在每个方法调用上复制的内容要多得多。

于 2009-02-28T01:17:54.623 回答
6

我要寻找的第一件事是确保您已明确实现 Equals 和 GetHashCode。未能做到这一点意味着它们中的每一个的运行时实现都会执行一些非常昂贵的操作来比较两个结构实例(在内部它使用反射来确定每个私有字段,然后检查它们是否相等,这会导致大量分配) .

不过,一般来说,您能做的最好的事情是在分析器下运行您的代码并查看慢速部分在哪里。这可能是一次令人大开眼界的体验。

于 2009-02-28T03:12:03.150 回答
4

您是否对应用程序进行了概要分析?分析是查看实际性能问题所在的唯一可靠方法。有些操作通常在结构上更好/更差,但除非您进行概要分析,否则您只会猜测问题所在。

于 2009-02-28T01:40:30.980 回答
2

虽然功能相似,但结构通常比类更有效。如果类型作为值类型比引用类型执行得更好,则应该定义结构而不是类。

具体来说,结构类型应满足所有这些标准:

  • 逻辑上表示单个值
  • 实例大小小于 16 字节
  • 创建后不会更改
  • 不会被强制转换为引用类型
于 2009-04-17T15:00:26.287 回答
0

我基本上将结构用于参数对象,从函数返回多条信息,并且......没有别的。不知道是“对”还是“错”,但我就是这样做的。

于 2009-02-28T06:05:38.973 回答
0

我自己的光线追踪器也使用 struct Vectors(虽然不是 Rays),并且将 Vector 更改为类似乎对性能没有任何影响。我目前正在为向量使用三个双打,所以它可能比它应该的要大。不过要注意一件事,这可能很明显,但不适合我,那就是在 Visual Studio 之外运行程序。即使您将其设置为优化的发布版本,如果您在 VS 之外启动 exe,您也可以获得巨大的速度提升。您所做的任何基准测试都应考虑到这一点。

于 2009-04-17T10:13:03.887 回答
-1

如果结构很小,并且一次不存在太多,它应该将它们放在堆栈上(只要它是局部变量而不是类的成员)而不是堆上,这意味着 GC 不t 需要被调用并且内存分配/释放应该几乎是瞬时的。

当将结构作为参数传递给函数时,结构被复制,这不仅意味着更多的分配/解除分配(从堆栈,这几乎是瞬时的,但仍然有开销),而且只是在 2 个副本之间传输数据的开销. 如果您通过引用传递,这不是问题,因为您只是告诉它从哪里读取数据,而不是复制它。

我对此不是 100% 确定的,但我怀疑通过“out”参数返回数组也可能会提高速度,因为堆栈上的内存是为它保留的,不需要作为堆栈复制在函数调用结束时“展开”。

于 2009-02-28T16:14:15.420 回答
-5

您还可以将结构体转换为 Nullable 对象。无法创建自定义类

作为

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

结构可以为空的地方

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

但是您将(显然)失去所有继承功能

于 2009-02-28T01:26:43.213 回答