Search Unity

Structs vs Classes

Discussion in 'Scripting' started by tristanrichter, Jul 28, 2015.

  1. tristanrichter

    tristanrichter

    Joined:
    Oct 28, 2013
    Posts:
    15
    What would be the advantages and disadvantages of implementing Vector3 as a class and as a struct?
     
  2. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Reference types (classes) generally have more significant GC implications than value types (structs).
     
  3. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
  4. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
  5. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    Wow, something is not right here.
     
    A.Killingbeck likes this.
  6. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    How to Avoid Allocating Memory
    Every time an object is created, memory is allocated. Very often in code, you are creating objects without even knowing it.

    • Debug.Log("boo" + "hoo"); creates an object.
      • Use System.String.Empty instead of "" when dealing with lots of strings.
    • Immediate Mode GUI (UnityGUI) is slow and should not be used at any time when performance is an issue.
    • Difference between class and struct:
    Classes are objects and behave as references. If Foo is a class and

    Foo foo = new Foo();
    MyFunction(foo);

    then MyFunction will receive a reference to the original Foo object that was allocated on the heap. Any changes to foo inside MyFunction will be visible anywhere foo is referenced.
    (I think here they mean to type Structs, not Classes)
    Classes are data and behave as such. If Foo is a struct and

    Foo foo = new Foo();
    MyFunction(foo);

    then MyFunction will receive a copy of foo. foo is never allocated on the heap and never garbage collected. If MyFunction modifies it’s copy of foo, the other foo is unaffected.

    • Objects which stick around for a long time should be classes, and objects which are ephemeral should be structs. Vector3 is probably the most famous struct. If it were a class, everything would be a lot slower.
     
  7. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,568
    There are lots of subtle differences but the main thing is:

    A) Classes are allocated on the heap and therefor subject to garbage collection
    B) Structs are allocated on the stack and do not feed the GC

    However, that doesn't mean that everything should be a struct. Any time you need a reference to something it should be a class. Also, when you pass a class into a method call it actually passes the 32-bit pointer (an integer) to the memory location of the class. Be mindful of what data types and how much data you have in your structs. If it is larger than 32-bits and you're passing it around a lot, then you're actually potentially being less efficient because you're making copies of that data and actually consuming more memory (though still not feeding the GC).

    Also, there are cases when structs can end up on the heap. If they are fields that belong to a class then that class is allocated on the heap and the fields are as well meaning your struct will actually live on the heap.
     
    Nigey likes this.
  8. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Classes are for big, important thingies. You only get one copy of each instance of a class. It lives in the heap.

    Structs are for small, throw away thingies. You get a new copy every time you pass it around. It lives in the stack.

    As a Vector3 is small and often throw away, it makes sense for it to be a struct.
     
    lordofduct likes this.
  9. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,568
    I was trying to avoid using too many highly technical terms such as "thingies".
     
  10. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,537
    What if I have a bunch of little thingies that never go away? Or a bunch of big thingies that i throw away a lot?
     
  11. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    You choose. I would suggest a class as being more efficient.

    This is a loose/loose situation. A class will kill you on GC. A struct will kill you on time to copy around. I would bench mark both and see what comes out best. Its also a good candidate for using a pool
     
  12. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,568
    I'm sorry sir, but unless it fits in the overhead compartment you're going to have to check your data.

    Very true, you're better to use an object pool and recycle those big thingies.
     
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Little thingies never going away - for me it would depend on what I'm doing with it. If they have references in them, I'll probably make them a class. But yeah, I agree with BoredMormon, this is a you choose situation... neither really have a benefit in this case with out more detail about what they specifically do.

    An example I might lean towards class is that it needs to implement an interface, and I know for a fact that where ever it gets used it's going to be referenced as an interface. This means it will be boxed. So might as well just make it a class seeing as it'll be living on the heap anyways... so rather than having an implied boxed reference, go with the full reference.


    A bunch of big thingies that you throw away a lot - now this one is a problem I run into a lot, it may also be small class just like above with the interface.

    Here I'll implement an object pooling creational pattern. I'll have a static pool of objects that cache them for reuse. I actually have the pool class set up for easy implementation:

    https://github.com/lordofduct/space...SpacepuppyBase/Collections/ObjectCachePool.cs

    One place I use it a bit is with my RadicalCoroutine class:
    https://github.com/lordofduct/space...lob/master/SpacepuppyBase/RadicalCoroutine.cs

    With it I allow implementing your own wait instructions via my IRadicalYieldInstruction interface, and I treat everything as that. For ease I wrap enumerators that come back in an implementation of that, which is small but exists for small periods of time. So you can see how I cache them here:

    Code (csharp):
    1.  
    2.         private class EnumWrapper : IRadicalYieldInstruction, IPooledYieldInstruction
    3.         {
    4.  
    5.             private static com.spacepuppy.Collections.ObjectCachePool<EnumWrapper> _pool = new com.spacepuppy.Collections.ObjectCachePool<EnumWrapper>(-1, () => new EnumWrapper());
    6.             public static EnumWrapper Create(IEnumerator e)
    7.             {
    8.                 var w = _pool.GetInstance();
    9.                 w._e = e;
    10.                 w._complete = false;
    11.                 return w;
    12.             }
    13.  
    14.  
    15.             internal IEnumerator _e;
    16.             private bool _complete;
    17.  
    18.             private EnumWrapper()
    19.             {
    20.  
    21.             }
    22.  
    23.             public bool Tick(out object yieldObject)
    24.             {
    25.                 if(_e.MoveNext())
    26.                 {
    27.                     yieldObject = _e.Current;
    28.                     return true;
    29.                 }
    30.                 else
    31.                 {
    32.                     _complete = true;
    33.                     yieldObject = null;
    34.                     return false;
    35.                 }
    36.             }
    37.  
    38.             public bool IsComplete
    39.             {
    40.                 get
    41.                 {
    42.                     return _complete;
    43.                 }
    44.             }
    45.  
    46.             void System.IDisposable.Dispose()
    47.             {
    48.                 _e = null;
    49.                 _pool.Release(this);
    50.             }
    51.         }
    52.  
    Or I have an implementation of WaitForSeconds called WaitForDuration. With this I allow integrating my SPTime class and define which kind of time (real, gametime, or a custom time as I allow creating multiple custom independently scaling times). Again these get used a lot, so I cache them:

    https://github.com/lordofduct/space...er/SpacepuppyBase/IRadicalYieldInstruction.cs
    Code (csharp):
    1.  
    2.  
    3.     /// <summary>
    4.     /// Represents a duration of time to wait for that can be paused, and can use various kinds of TimeSuppliers.
    5.     /// NOTE - this yield instruction is pooled, NEVER store one for reuse. The pool takes care of that for you. Instead
    6.     /// use the static factory methods.
    7.     /// </summary>
    8.     public class WaitForDuration : IPausibleYieldInstruction, IPooledYieldInstruction, IProgressingYieldInstruction
    9.     {
    10.  
    11.         #region Fields
    12.  
    13.         private ITimeSupplier _supplier;
    14.         private float _t;
    15.         private float _cache;
    16.         private float _dur;
    17.  
    18.         #endregion
    19.  
    20.         #region CONSTRUCTOR
    21.  
    22.         private WaitForDuration()
    23.         {
    24.  
    25.         }
    26.  
    27.         private void Init(float dur, ITimeSupplier supplier)
    28.         {
    29.             _supplier = supplier ?? SPTime.Normal;
    30.             _dur = dur;
    31.             this.Reset();
    32.         }
    33.  
    34.         #endregion
    35.  
    36.         #region Properties
    37.  
    38.         public float Duration { get { return _dur; } }
    39.  
    40.         public float CurrentTime { get { return (float.IsNaN(_t)) ? _cache : (_supplier.Total - _t) + _cache; } }
    41.  
    42.         #endregion
    43.  
    44.         #region IEnumerator Interface
    45.  
    46.         bool IRadicalYieldInstruction.IsComplete
    47.         {
    48.             get { return this.CurrentTime >= _dur; }
    49.         }
    50.  
    51.         float IProgressingYieldInstruction.Progress
    52.         {
    53.             get { return Mathf.Clamp01(this.CurrentTime / _dur); }
    54.         }
    55.  
    56.         bool IRadicalYieldInstruction.Tick(out object yieldObject)
    57.         {
    58.             yieldObject = null;
    59.             return this.CurrentTime < _dur;
    60.         }
    61.  
    62.         public void Reset()
    63.         {
    64.             _t = _supplier.Total;
    65.             _cache = 0f;
    66.         }
    67.  
    68.         void IPausibleYieldInstruction.OnPause()
    69.         {
    70.             _cache = (_supplier.Total - _t) + _cache;
    71.             _t = float.NaN;
    72.         }
    73.  
    74.         void IPausibleYieldInstruction.OnResume()
    75.         {
    76.             _t = Time.time;
    77.         }
    78.  
    79.         #endregion
    80.  
    81.         #region IPooledYieldInstruction Interface
    82.  
    83.         void System.IDisposable.Dispose()
    84.         {
    85.             _pool.Release(this);
    86.         }
    87.  
    88.         #endregion
    89.  
    90.         #region Static Factory
    91.  
    92.         private static com.spacepuppy.Collections.ObjectCachePool<WaitForDuration> _pool = new com.spacepuppy.Collections.ObjectCachePool<WaitForDuration>(1000, () => new WaitForDuration());
    93.  
    94.         public static WaitForDuration Seconds(float seconds, ITimeSupplier supplier = null)
    95.         {
    96.             var w = _pool.GetInstance();
    97.             w.Init(seconds, supplier);
    98.             return w;
    99.         }
    100.  
    101.         public static WaitForDuration FromWaitForSeconds(WaitForSeconds wait, bool returnNullIfZero = true)
    102.         {
    103.             var dur = ConvertUtil.ToSingle(DynamicUtil.GetValue(wait, "m_Seconds"));
    104.             if (returnNullIfZero && dur <= 0f)
    105.             {
    106.                 return null;
    107.             }
    108.             else
    109.             {
    110.                 var w = _pool.GetInstance();
    111.                 w.Init(dur, SPTime.Normal);
    112.                 return w;
    113.             }
    114.         }
    115.  
    116.         #endregion
    117.  
    118.     }
    119.  
    If you're interested in that ITimeSupplier/SPTime thing:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/SPTime.cs

    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/ITimeSupplier.cs
     
    Last edited: Jul 28, 2015
    Darkkingdom and Dustin-Horne like this.