什么时候结构的答案?

时间:2009-02-28 01:05:48

标签: c# performance struct raytracing struct-vs-class

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

是什么给出的?它们都很小(Vector为3个浮点数,Ray为2个向量),不要过度复制。当然,我确实将它们传递给方法,但这是不可避免的。那么在使用结构时会导致性能下降的常见缺陷是什么?我已阅读this MSDN文章,内容如下:

  

运行此示例时,您将看到struct循环的速度提高了几个数量级。但是,当您将ValueTypes视为对象时,请注意使用ValueTypes。这会给你的程序增加额外的装箱和拆箱开销,并且最终会比你坚持使用物品时花费更多!要查看此操作,请修改上面的代码以使用foos和bar数组。你会发现性能或多或少相等。

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

我正在运行.NET 3.5 SP1,我认为修复了一个问题,即结构方法没有内联,所以也不可能。

所以基本上:任何提示,需要考虑的事项和要避免的事情?

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

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%。为什么这对我的光线跟踪器不同,我不知道。

12 个答案:

答案 0 :(得分:26)

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

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


<强>结构

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

VVVV

<强>类

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

PPPP

.. v..v ... v.v ..

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

答案 1 :(得分:11)

在关于何时使用struct的建议中,它表示它不应该大于16个字节。您的Vector是12个字节,接近极限。 Ray有两个Vector,它是24个字节,显然超过了建议的限制。

当结构体大于16个字节时,不能再使用一组指令有效地复制它,而是使用循环。因此,通过传递这个“魔法”限制,当您传递结构时,实际上比传递对象的引用要多得多。这就是为什么代码在类中更快的原因,尽管在分配对象时会有更多的开销。

Vector可能仍然是一个结构体,但Ray太大而不能像结构一样好用。

答案 2 :(得分:9)

任何关于.NET泛型之前的装箱/拆箱的文章都可以用一些盐。通用集合类型消除了对值类型装箱和拆箱的需要,这使得在这些情况下使用结构更有价值。

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

答案 3 :(得分:6)

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

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

答案 4 :(得分:6)

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

  

你创造了数百万

  

当我需要时,我会将它们传递给方法

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

答案 5 :(得分:6)

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

通常,您可以做的最好的事情是在分析器下运行代码并查看慢速部件的位置。这可以让人大开眼界。

答案 6 :(得分:4)

您是否对应用程序进行了分析?分析是了解实际性能问题的唯一确定方法。有些操作通常在结构上更好/更差但除非你描述你只是猜测问题是什么。

答案 7 :(得分:2)

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

具体而言,结构类型应符合所有这些标准:

  • 逻辑上代表单个值
  • 实例大小小于16个字节
  • 创建后不会更改
  • 不会转换为引用类型

答案 8 :(得分:0)

我基本上为参数对象使用结构,从函数返回多条信息,而......没有别的。不知道它是“正确”还是“错误”,但这就是我所做的。

答案 9 :(得分:0)

我自己的光线跟踪器也使用struct Vectors(虽然不是Rays),将Vector更改为class似乎对性能没有任何影响。我目前正在为矢量使用三个双打,所以它可能比它应该更大。有一点需要注意,这可能是显而易见的,但它不适合我,那就是在visual studio之外运行程序。即使你将它设置为优化版本构建,如果你在VS之外启动exe,你也可以获得巨大的速度提升。你做的任何基准测试都应该考虑到这一点。

答案 10 :(得分:-1)

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

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

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

答案 11 :(得分:-5)

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

作为

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

结构可以为空的

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

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