何时使用结构体?

36
我正在进行一个兴趣项目——光线追踪器,最初我在我的矢量和射线对象中使用结构体,并且认为光线追踪器是使用它们的完美场景:你创建了数百万个它们,它们的生命周期不超过单个方法,它们很轻便。然而,仅仅将Vector和Ray上的'struct'更改为'class',我得到了非常显著的性能提升。
怎么回事?它们都很小(3个浮点数用于Vector,2个Vector用于Ray),不会被过度复制。当然,我需要在需要时将它们传递给方法,但这是不可避免的。那么,在使用结构体时会导致性能下降的常见陷阱是什么?我读过thisMSDN文章,其中说:
“运行此示例时,您会发现结构体循环速度要快几个数量级。但是,当您将ValueTypes像对象一样处理时,要注意不要这样做。这会给您的程序添加额外的装箱和取消装箱开销,并最终比坚持使用对象更费钱!要查看此操作,请修改上面的代码以使用foo和bar的数组。您会发现性能或多或少相等。”
但是,这篇文章已经相当老了(2001年),而整个“将它们放入数组中会导致装箱/取消装箱”让我感到奇怪。这是真的吗?然而,我确实预先计算了主光线并将其放入了一个数组中,所以我参考了这篇文章,当我需要时计算主光线并从未将其添加到数组中,但是它没有改变任何事情:使用类仍然比结构体快1.5倍。
我正在运行.NET 3.5 SP1,我相信该版本修复了结构体方法从未内联的问题,因此也不可能是这个问题。
所以基本上:有什么提示、需要考虑的事情和要避免的?
编辑:如一些答案中建议的那样,我设置了一个测试项目,在其中尝试将结构体作为ref传递。添加两个向量的方法:
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);
}

对于每个 I,我都得到了以下基准测试方法的一个变化:

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优化为传递此结构的最佳方式?

编辑2:顺便说一下,在我的测试项目中使用结构体比使用类快大约50%。我不知道这在我的光线追踪器中为什么会有所不同。


祝你在项目中好运,光线追踪器是我即将要处理的东西。 - GurdeepS
请参见https://dev59.com/P3RB5IYBdhLWcg3wxZ1K#521343(特别是我的答案 :)) - Brian
2
创建一个光线追踪器非常有趣。我发现令人着迷的是,你可以从一堆浮点数和相对简单的向量数学中创建图像。 - JulianR
1
我认为这篇文章并没有说将结构体放入数组会导致装箱。它提醒我们,在需要对象的地方使用结构体确实会导致装箱;例如,如果你将它们传递给一个期望类型为 object 的参数的方法。 - ILoveFortran
可能是在C#中何时使用结构体?的重复问题。 - nawfal
12个回答

-1
如果结构体很小,并且同时存在的数量不太多,应该将它们放在堆栈上(只要是局部变量而不是类的成员),而不是堆中。这意味着垃圾回收器不需要被调用,内存分配/释放几乎是瞬间完成的。
当将结构体作为参数传递给函数时,会复制结构体。这不仅意味着需要进行更多的分配/释放(来自堆栈,虽然几乎是瞬间完成的,但仍有开销),还有在两个副本之间传输数据的开销。如果通过引用传递,则这不是问题,因为您只是告诉它从哪里读取数据,而不是复制它。
我不能百分之百确定,但我认为通过“out”参数返回数组也可能会提高速度,因为内存在堆栈中保留,不需要像堆一样被复制,因为堆栈在函数调用结束时会被“展开”。

我经常听说结构体存在于堆栈上,而不是垃圾回收堆上;但这并不完全正确。它们不是独立存在的,但它们可以作为另一个引用类型对象的一部分存在于堆上。 - ILoveFortran
在通过复制方式传递结构体时,不需要进行进一步的分配 - 相应数量的空间在调用前已在堆栈上保留。但是,仍然需要进行复制,这就使得通过复制方式传递结构体更加昂贵,而不是分配所导致的。 - ILoveFortran
我确实说过,假设它们是局部变量(正如问题中所详述的),它应该放在那里。此外,内存确实需要从堆栈中分配,这基本上不是问题,但仍然存在轻微的开销。 - Grant Peters

-5
您还可以将结构体转换为可空对象。但自定义类别无法创建。
Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

带有结构体的变量可以为空

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

但你将会(显然)失去所有的继承特性


4
这只是将结构体包装在类中,从而调用GC。你最好将结构体改为类,因为它会产生同样的效果,并且可能稍微不那么令人困惑。 - Grant Peters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接