什么时候结构的答案?

Jul*_*anR 32 c# performance struct raytracing struct-vs-class

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

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

运行此示例时,您将看到struct循环的速度提高了几个数量级.但是,当您将ValueTypes视为对象时,请注意使用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);
}
Run Code Online (Sandbox Code Playgroud)

对于每个我得到以下基准方法的变体:

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. 
}
Run Code Online (Sandbox Code Playgroud)

所有人似乎表现得非常相同.是否有可能它们被JIT优化为通过这个结构的最佳方式?

EDIT2:我必须在我的测试项目中使用结构的方式注意不是使用的一类快了约50%.为什么这对我的光线跟踪器不同,我不知道.

ILo*_*ran 26

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

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


结构

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

VVVV

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

PPPP

..v..v ...... VV.

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


Guf*_*ffa 11

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

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

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

  • 将Vector作为类而Ray作为结构将使Ray包含两个引用.这将在尺寸方面起作用,但您可能会获得一些令人惊讶的语义效果.使两个结构都使得Ray结构超出了大小限制. (2认同)
  • 结构体处理针对结构体为 16 字节或更少的情况进行了优化;因此,17 字节结构的性能将明显低于 16 字节结构的性能。另一方面,如果避免按值传递结构(尽可能通过“ref”传递),即使 100 字节的结构也可以比 100 字节的类表现得更好。 (2认同)

Eri*_*bes 9

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

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


Tra*_*ony 6

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

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

  • 自从我问起这个以来,我已经变得更有经验了.在重写光线跟踪器时,通过引用传递*all*结构确实要快得多.但是,它确实让我的代码变得非常难看.小的数学单行语句变成了Vector3.Add/Subtract/Multiply/etc的5个带衬里的块,我不得不求助于很多公共字段,因为属性不能通过ref传递(理所当然,这是一个方法之后所有).现在它真的很快,在非常简单的场景中实时帧率. (3认同)
  • 通过参考传递?拥有一个结构的意义在于它是如此之小以至于复制它比使用它的引用开销更有效... (2认同)

And*_*are 6

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

你创造了数百万

当然,我确实将它们传递给了方法

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

  • 显然不是 :) (2认同)
  • 如果一个人创建了数百万个不同的实例,那么结构将提供比不可变类更好的性能.差不多总是.类可以获胜的唯一时间是大多数引用指向可以与其他引用共享的实例.有一百万个引用都引用三个不可变类实例中的一个可能比拥有一百万个结构更好,这些结构都"发生"以保存三个字段组合中的一个,但如果百万个引用都指向差异类实例,那么什么都没有是通过使用类而不是结构获得的. (2认同)

小智 6

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

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