Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.
    Dismiss Notice
  2. Dismiss Notice

Opinions about tokenizing

Discussion in 'Scripting' started by lordofduct, Oct 21, 2015.

  1. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,631
    So I want to get some opinions from some of y'all (@eisenpony, @BoredMormon, I know y'all will probably have some thoughts).

    So I find myself wanting to tokenize common components like 'Transform':
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Geom/Trans.cs

    And currently I'm writing a token for the Camera.

    Thing is I can't decide if I should use a struct or not. Microsoft gives 4 good lines to when:
    1) it logically describes a value (it does, it's a token)
    2) it is immutable (again, yep, it's a token)
    3) it will not be boxed frequently (it shouldn't be... as a token its hardly polymorphic)
    4) it has an instance size under 16 bytes (ugh...)

    So that last one there is my stickler. But it's not like its always a always do, I mean really... would you make a double precision Vector3 a class despite it being 24 bytes? It's a value... it should be a value. Just like how Matrix4x4 is 64 bytes in size.

    This is why in the case of my Transform token Trans linked above I went with struct. Despite it being 40 bytes in size. It just made most sense.

    But here I am about to make a token that is in total 71 bytes! It's not all the Camera properties, but it's at least the ones that are editable in the inspector.

    My design side of me is saying "duh, it's a struct, it's a set of immutable values used for a short term token... and think about that GC". But the other part of me is like, "holy jeebus, that's one giant struct!".

    I'm also considering the idea that I could create a 'CameraTokenExtended' with all the properties of a Camera to tokenize... and if I wanted that to inherit I'd have to use a class (structs no inherit). Anyways, with all those extra values, that's one unwieldly struct.

    I'll be honest, I'm leaning class for this last reason alone.

    What do y'all think?


    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. namespace com.spacepuppy.Cameras
    5. {
    6.  
    7.     /// <summary>
    8.     /// Stores the state of a Camera.
    9.     ///
    10.     /// This type is a class rather than a struct, you should create an object for the token and recycle as needed.
    11.     /// </summary>
    12.     [System.Serializable()]
    13.     public class CameraToken
    14.     {
    15.  
    16.         #region Fields
    17.  
    18.         public CameraClearFlags clearFlags;
    19.         public Color backgroundColor;
    20.         public LayerMask cullingMask;
    21.         public bool orthographic;
    22.         public float orthographicSize;
    23.         public float fieldOfView;
    24.         public float nearClipPlane;
    25.         public float farClipPlane;
    26.         public Rect rect;
    27.         public float depth;
    28.         public RenderingPath renderingPath;
    29.         public RenderTexture targetTexture;
    30.         public bool useOcclusionCulling;
    31.         public bool hdr;
    32.  
    33.         #endregion
    34.  
    35.         #region CONSTRUCTOR
    36.  
    37.         public CameraToken()
    38.         {
    39.  
    40.         }
    41.  
    42.         public CameraToken(Camera camera)
    43.         {
    44.             this.CopyFrom(camera);
    45.         }
    46.  
    47.         #endregion
    48.        
    49.         #region Methods
    50.  
    51.         public void CopyTo(Camera camera)
    52.         {
    53.             camera.clearFlags = this.clearFlags;
    54.             camera.backgroundColor = this.backgroundColor;
    55.             camera.cullingMask = this.cullingMask;
    56.             camera.orthographic = this.orthographic;
    57.             camera.orthographicSize = this.orthographicSize;
    58.             camera.fieldOfView = this.fieldOfView;
    59.             camera.nearClipPlane = this.nearClipPlane;
    60.             camera.farClipPlane = this.farClipPlane;
    61.             camera.rect = this.rect;
    62.             camera.depth = this.depth;
    63.             camera.renderingPath = this.renderingPath;
    64.             camera.targetTexture = this.targetTexture;
    65.             camera.useOcclusionCulling = this.useOcclusionCulling;
    66.             camera.hdr = this.hdr;
    67.         }
    68.  
    69.         public void CopyFrom(Camera camera)
    70.         {
    71.             this.clearFlags = camera.clearFlags;
    72.             this.backgroundColor = camera.backgroundColor;
    73.             this.cullingMask = camera.cullingMask;
    74.             this.orthographic = camera.orthographic;
    75.             this.orthographicSize = camera.orthographicSize;
    76.             this.fieldOfView = camera.fieldOfView;
    77.             this.nearClipPlane = camera.nearClipPlane;
    78.             this.farClipPlane = camera.farClipPlane;
    79.             this.rect = camera.rect;
    80.             this.depth = camera.depth;
    81.             this.renderingPath = camera.renderingPath;
    82.             this.targetTexture = camera.targetTexture;
    83.             this.useOcclusionCulling = camera.useOcclusionCulling;
    84.             this.hdr = camera.hdr;
    85.         }
    86.  
    87.         #endregion
    88.        
    89.     }
    90. }
    91.  
    92.  
    93.  
     
    Last edited: Oct 21, 2015
  2. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    <Tootles off to google what a token is. Will be back soon>

    Edit: And back. I now have the concept of what you are doing. I'm still not sure why, which may influence my answer.

    My gut is pushing this towards using a struct. Mainly because of the GC issues.

    But the more I'm reading about it the more I'm leaning towards building a stress test for your use case and benchmarking/profiling. Google doesn't seem to have any idea why the 16-byte limit was recommended.
     
    Last edited: Oct 21, 2015
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,631
    A representation of an object properties for caching purposes.

    A use case of this CameraToken in out current game is we want to zoom in on an item in our game when you click it (its a creature sim game). But when we zoom in, the artist says we need to have control over several of the camera settings so that the camera projects differently when we go up close.

    With the token I can cache the current state of the camera, zoom in to the new values, then when clicked off we return back the values stored in the token.
     
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,369
    Are you going to be using this in a context where performance of struct vs. performance of class will be important? As in, are you going to tokenize every bullet in a bullethell game? It might be me, but I'm not finding the use case where there's going to be a lot of these floating around.

    Because of that, it comes down to the question "do you want to pass these things by value or by reference?".
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,631
    Well, if I was going to cache bullets, that'd be my Trans token. Which is already a struct, because it's what makes most sense there.

    The one I'm considering right now is the CameraToken.

    And with that one I described a use case:

    The generalized use case for either is if I want to store the current state of some Transform or Camera, so I can than modify its state, and have a record of what to return to when I'm done.

    A good Trans use case would be say there's an item in the scene, and when the player clicks it, it floats up towards the camera and is in front of the player for them to observe up close. Then when they're done, they click off, and the item returns to its original position back in the scene. Before picking it up, I'd make a token of the Transform, then I'd tween it up in front of the camera, then when off click, I'd tween it back to the values that were in the token. (yeah, it's sort of the opposite of my cameratoken example... resulting in different user experiences)
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,631
    Kiwasi likes this.
  7. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I think Baste brought up a good point. It seems very unlikely you will have a lot of these tokens lying around, so I don't see value in worrying about size.

    Based on your own analysis, the other three guides seem to point towards making this type a struct, so that's probably the way I'd go.

    As for inheritance, these days I tend to avoid it wherever I can. I don't think inheritance is bad, I just think I'm biased towards using it, so I intentionally try to avoid it until it becomes inconvenient to do so. Since you previously said "a token is hardly polymorphic", I'm curious: what led you to believe you would need to support inheritance?
     
  8. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,369
    Remember that Unity can't handle serialization of arrays of a base class. Putting some TransTokens and CameraTokens in a Token[] won't work. If you're going to mix polymorphism and Serializeable custom classes, you'll need to serialize through OnBeforeSerialize/AfterDeserialize, and put the data into some structure(s) in the base class.

    Or you could just not be Serializeable, to save yourself the pain. Again, it depends on use.
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,631
    Oh, I wasn't talking about a 'Token' class that Trans and CameraToken were inheriting from. Trans is still, and always will, be a struct.

    I was talking a CameraToken and CameraTokenExtended, the latter containing more properties.

    And the tokens aren't generally supposed used for commonly serialized situations (though, yeah, the inclusion of the attribute is probably confusing in that regard). They're a runtime thing, to toss data around, not to store long term. So a limitation in the unity serializer doesn't concern me personally.


    Thanks for your inputs though, keep em' coming.

    I'd love to get any opinions on why Microsoft creates a guideline with such a low memory footprint.

    It makes me think the guideline is rather outdated, like maybe they wrote it early in the .Net life, when 64-bit machines weren't common.

    The article in question is here:
    https://msdn.microsoft.com/en-us/library/ms229017(v=vs.110).aspx

    (despite the 2009 portion copyright on it)
     
  10. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I was thinking about this again and it seems to me the guidance is probably related to how value types are copied during runtime. I'll bet that the 16 byte recommendation came from an implementation detail regarding how the c# compiler copied structs at the time of writing.

    It seemed that copying small values (like ints) would be a good target for optimization tricks, so I wanted to see if I could detect them by copying structs of different sizes. I ran this test on my 64 bit machine with code compiled in VS2015 C#6.

    Here are my results from code targeted at an x86 machine
    2 bytes 395
    4 bytes 147
    8 bytes 452
    16 bytes 651
    20 bytes 893
    24 bytes 1109
    32 bytes 1152
    40 bytes 1667
    48 bytes 1947
    56 bytes 5630
    64 bytes 6020
    72 bytes 6419

    upload_2015-10-29_12-31-53.png

    Here are my results from code targeted at x64
    2 bytes 159
    4 bytes 149
    8 bytes 148
    16 bytes 422
    20 bytes 638
    24 bytes 596
    32 bytes 832
    40 bytes 901
    48 bytes 1134
    56 bytes 1436
    64 bytes 1459
    72 bytes 5673

    upload_2015-10-29_12-32-4.png

    For the x86 code, it looks like time for struct copies are linearly related to their size up to 48 bytes. I suspect the spike at 56 bytes is caused by a switch in the algorithm used. Maybe a changing to an iterative memory copy loop rather than simple memory moves. I also noticed a weird blip at 4 bytes (int) which I'll bet is some crazy optimization for int's since they are so common.

    For the x64 code, it looks like structs 8 bytes and under have a massive advantage; they all get copied very fast with almost no correlation between size and copy time. This must be using some memory move that is good for values up to 8 bytes. After that, things start increasing linearly so probably just repeating several of these memory move commands. 72 bytes is where the spike happens for x64 code, so I suspect 64 bytes is the threshold for switching from multiple memory move commands to a different algorithm.

    Anyways, I'm thinking the document you read is simply out of date and besides, this is going to be compiler implementation specific for sure. I'd be curious to see #'s from Unity.

    Code (csharp):
    1. using System;
    2. using System.Diagnostics;
    3.  
    4. namespace StructTests
    5. {
    6.     class Program
    7.     {
    8.         static void Main(string[] args)
    9.         {
    10.             var p = new Program();
    11.  
    12.             Stopwatch watch = new Stopwatch();
    13.  
    14.  
    15.             var s2 = new S2();
    16.             watch.Reset();
    17.             watch.Start();
    18.             for (int j = 0; j < 500000000; j++)
    19.             {
    20.                 p.Do(s2);
    21.             }
    22.             watch.Stop();
    23.  
    24.             Console.WriteLine($"2 bytes \t{watch.ElapsedMilliseconds}");
    25.  
    26.  
    27.             var s4 = new S4();
    28.             watch.Reset();
    29.             watch.Start();
    30.             for (int j = 0; j < 500000000; j++)
    31.             {
    32.                 p.Do(s4);
    33.             }
    34.             watch.Stop();
    35.  
    36.             Console.WriteLine($"4 bytes \t{watch.ElapsedMilliseconds}");
    37.  
    38.  
    39.             var s8 = new S8();
    40.             watch.Reset();
    41.             watch.Start();
    42.             for (int j = 0; j < 500000000; j++)
    43.             {
    44.                 p.Do(s8);
    45.             }
    46.             watch.Stop();
    47.  
    48.             Console.WriteLine($"8 bytes \t{watch.ElapsedMilliseconds}");
    49.  
    50.  
    51.             var s16 = new S16();
    52.             watch.Reset();
    53.             watch.Start();
    54.             for (int j = 0; j < 500000000; j++)
    55.             {
    56.                 p.Do(s16);
    57.             }
    58.             watch.Stop();
    59.  
    60.             Console.WriteLine($"16 bytes \t{watch.ElapsedMilliseconds}");
    61.  
    62.  
    63.             var s20 = new S20();
    64.             watch.Reset();
    65.             watch.Start();
    66.             for (int j = 0; j < 500000000; j++)
    67.             {
    68.                 p.Do(s20);
    69.             }
    70.             watch.Stop();
    71.  
    72.             Console.WriteLine($"20 bytes \t{watch.ElapsedMilliseconds}");
    73.  
    74.  
    75.             var s24 = new S24();
    76.             watch.Reset();
    77.             watch.Start();
    78.             for (int j = 0; j < 500000000; j++)
    79.             {
    80.                 p.Do(s24);
    81.             }
    82.             watch.Stop();
    83.  
    84.             Console.WriteLine($"24 bytes \t{watch.ElapsedMilliseconds}");
    85.  
    86.  
    87.             var s32 = new S32();
    88.             watch.Reset();
    89.             watch.Start();
    90.             for (int j = 0; j < 500000000; j++)
    91.             {
    92.                 p.Do(s32);
    93.             }
    94.             watch.Stop();
    95.  
    96.             Console.WriteLine($"32 bytes \t{watch.ElapsedMilliseconds}");
    97.  
    98.  
    99.             var s40 = new S40();
    100.             watch.Reset();
    101.             watch.Start();
    102.             for (int j = 0; j < 500000000; j++)
    103.             {
    104.                 p.Do(s40);
    105.             }
    106.             watch.Stop();
    107.  
    108.             Console.WriteLine($"40 bytes \t{watch.ElapsedMilliseconds}");
    109.  
    110.  
    111.             var s48 = new S48();
    112.             watch.Reset();
    113.             watch.Start();
    114.             for (int j = 0; j < 500000000; j++)
    115.             {
    116.                 p.Do(s48);
    117.             }
    118.             watch.Stop();
    119.  
    120.             Console.WriteLine($"48 bytes \t{watch.ElapsedMilliseconds}");
    121.  
    122.  
    123.             var s56 = new S56();
    124.             watch.Reset();
    125.             watch.Start();
    126.             for (int j = 0; j < 500000000; j++)
    127.             {
    128.                 p.Do(s56);
    129.             }
    130.             watch.Stop();
    131.  
    132.             Console.WriteLine($"56 bytes \t{watch.ElapsedMilliseconds}");
    133.  
    134.  
    135.             var s64 = new S64();
    136.             watch.Reset();
    137.             watch.Start();
    138.             for (int j = 0; j < 500000000; j++)
    139.             {
    140.                 p.Do(s64);
    141.             }
    142.             watch.Stop();
    143.  
    144.             Console.WriteLine($"64 bytes \t{watch.ElapsedMilliseconds}");
    145.  
    146.  
    147.             var s72 = new S72();
    148.             watch.Reset();
    149.             watch.Start();
    150.             for (int j = 0; j < 500000000; j++)
    151.             {
    152.                 p.Do(s72);
    153.             }
    154.             watch.Stop();
    155.  
    156.             Console.WriteLine($"72 bytes \t{watch.ElapsedMilliseconds}");
    157.  
    158.             Console.ReadLine();
    159.         }
    160.  
    161.         S2 Do(S2 s)
    162.         {
    163.             return s;
    164.         }
    165.  
    166.         S4 Do(S4 s)
    167.         {
    168.             return s;
    169.         }
    170.  
    171.         S8 Do(S8 s)
    172.         {
    173.             return s;
    174.         }
    175.  
    176.         S16 Do(S16 s)
    177.         {
    178.             return s;
    179.         }
    180.  
    181.         S20 Do(S20 s)
    182.         {
    183.             return s;
    184.         }
    185.  
    186.         S24 Do(S24 s)
    187.         {
    188.             return s;
    189.         }
    190.  
    191.         S32 Do(S32 s)
    192.         {
    193.             return s;
    194.         }
    195.  
    196.         S40 Do(S40 s)
    197.         {
    198.             return s;
    199.         }
    200.  
    201.         S48 Do(S48 s)
    202.         {
    203.             return s;
    204.         }
    205.  
    206.         S56 Do(S56 s)
    207.         {
    208.             return s;
    209.         }
    210.  
    211.         S64 Do(S64 s)
    212.         {
    213.             return s;
    214.         }
    215.  
    216.         S72 Do(S72 s)
    217.         {
    218.             return s;
    219.         }
    220.     }
    221.  
    222.     struct S2
    223.     {
    224.         public Int16 B1_2;
    225.     }
    226.  
    227.     struct S4
    228.     {
    229.         public Int32 B1_4;
    230.     }
    231.  
    232.     struct S8
    233.     {
    234.         public Int64 B1_8;
    235.     }
    236.  
    237.     struct S16
    238.     {
    239.         public Int64 B1_8, B9_16;
    240.     }
    241.  
    242.     struct S20
    243.     {
    244.         public Int64 B1_8, B9_16;
    245.         public Int32 B17_20;
    246.     }
    247.  
    248.     struct S24
    249.     {
    250.         public Int64 B1_8, B9_16, B17_24;
    251.     }
    252.  
    253.     struct S32
    254.     {
    255.         public Int64 B1_8, B9_16, B17_24, B25_32;
    256.     }
    257.  
    258.     struct S40
    259.     {
    260.         public Int64 B1_8, B9_16, B17_24, B25_32, B33_40;
    261.     }
    262.  
    263.     struct S48
    264.     {
    265.         public Int64 B1_8, B9_16, B17_24, B25_32, B33_40, B41_48;
    266.     }
    267.  
    268.     struct S56
    269.     {
    270.         public Int64 B1_8, B9_16, B17_24, B25_32, B33_40, B41_48, B49_56;
    271.     }
    272.  
    273.     struct S64
    274.     {
    275.         public Int64 B1_8, B9_16, B17_24, B25_32, B33_40, B41_48, B49_56, B57_64;
    276.     }
    277.  
    278.     struct S72
    279.     {
    280.         public Int64 B1_8, B9_16, B17_24, B25_32, B33_40, B41_48, B49_56, B57_64, B65_72;
    281.     }
    282. }
     
    Last edited: Oct 29, 2015
    Baste and lordofduct like this.
  11. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974