代码之家  ›  专栏  ›  技术社区  ›  JulianR

结构是什么时候的答案?

  •  32
  • JulianR  · 技术社区  · 17 年前

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

    有什么好处?它们都很小(矢量为3个浮点数,射线为2个矢量),不要被过度复制。当然,我会在需要时将它们传递给方法,但这是不可避免的。那么,在使用结构时,哪些常见的陷阱会破坏性能呢?我读过 this MSDN文章中说:

    当您运行这个示例时,您将看到struct循环快了几个数量级。但是,当您将ValueTypes视为对象时,一定要小心使用它们。这会给您的程序增加额外的装箱和拆箱开销,并且最终会比您使用对象时花费更多的成本!要查看此操作,请修改上面的代码以使用foo和bar数组。你会发现性能大致相当。

    但是它已经很老了(2001年),整个“把它们放在一个数组中会导致装箱/拆箱”让我觉得很奇怪。这是真的吗?但是,我确实预先计算了主光线并将其放入数组中,因此我开始阅读本文,在需要时计算主光线,并且从未将其添加到数组中,但这并没有改变任何事情:使用类时,速度仍然快1.5倍。

    我运行的是.NET3.5SP1,我相信它解决了一个结构方法从来都没有出现过的问题,所以这也不可能。

    所以基本上:任何小窍门,要考虑的事情和应该避免的事情?

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

    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:我必须注意,在我的测试项目中使用structs 大约比使用类快50%。我不知道为什么我的光线跟踪器不同。

    12 回复  |  直到 5 年前
        1
  •  29
  •   ILoveFortran    17 年前

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


    结构

    VVV

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

    pppp

    …v…v…v.v。。

        2
  •  12
  •   Guffa    17 年前

    在关于何时使用结构的建议中,它指出结构不应大于16字节。你的向量是12字节,接近极限。射线有两个向量,即24字节,这显然超出了建议的限制。

    向量仍然可以是一个结构,但光线太大,无法作为结构正常工作。

        3
  •  9
  •   Erik Forbes    17 年前

        4
  •  7
  •   Andrew Hare    17 年前

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

    你创造了数百万个

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

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

        5
  •  7
  •   TraumaPony    17 年前

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

        6
  •  6
  •   Alexai Alexai    17 年前

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

    一般来说,你能做的最好的事情是在分析器下运行你的代码,看看那些慢的部分在哪里。这可能是一次令人大开眼界的经历。

        7
  •  4
  •   JaredPar    17 年前

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

        8
  •  2
  •   Gineer    17 年前

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

    • 逻辑上表示单个值
    • 实例大小小于16字节
    • 创建后将不会更改
    • 将不会强制转换为引用类型
        9
  •  0
  •   Instance Hunter    17 年前

    我基本上对参数对象使用结构,从函数返回多条信息,以及。。。没有别的了。我不知道这是“对”还是“错”,但我就是这么做的。

        10
  •  0
  •   Morten Christiansen    17 年前

    我自己的光线跟踪器也使用结构向量(虽然不是光线),将向量更改为类似乎不会对性能产生任何影响。我现在用三个双倍的向量,所以它可能比应该的要大。但有一件事需要注意,这可能是显而易见的,但这对我来说不是,那就是在VisualStudio之外运行程序。即使您将其设置为优化发布版本,如果您在VS之外启动exe,您也可以获得巨大的速度提升。您所做的任何基准测试都应该考虑到这一点。

        11
  •  -1
  •   Grant Peters    17 年前

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

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

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

        12
  •  -5
  •   BozoJoe Doug L.    17 年前

    还可以将结构设置为可为空的对象。无法创建自定义类

    Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>
    

    其中,具有结构的可为空

    Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>
    

    但是(显然)您将丢失所有继承特性