Search Unity

Garbage free string manipulation

Discussion in 'Scripting' started by Dave-Carlile, Oct 24, 2016.

  1. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
    Does anyone know of a good string formatting asset that can format strings with no memory allocations? Specifically concatenating strings and numbers. I know StringBuilder can help there, but that final ToString() is still problematic.
     
  2. Kalladystine

    Kalladystine

    Joined:
    Jan 12, 2015
    Posts:
    227
    Haven't heard of any...
    Only 2 things that come to mind is either using unsafe/fixed and manipulating the underlying arrays, which is kind of risky and I'd hesitate to go that route (even though it is possible, I've seen it work, but it's just so easy to mess up :( )...
    Or using reflection to bypass the .ToString(), essentially exchanging some speed for memory allocations. Some searching showed this. Haven't tested it, but it might be helpful.
     
  3. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,435
  4. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
    I've used that in the past, but the reflection used to get at the StringBuilder internals is problematic. I abandoned it after it stopped working after a Unity upgrade. The formatting part is good though - garbage free numbers to strings.
     
  5. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Yeah, that is just a wrapper, and still has to have ToString called.

    Furthermore, when dealing with strings something has to be created. A string is still an object, and therefore will still be garbage when you're done with it.

    But you want to reduce that garbage. Thing is, allowing for a mutable string that doesn't create garbage is rather difficult. Since a string is bit like a char array (with helper methods added on), and array's are inherently static in size, to append/prepend implies a new array (hence a new string).

    StringBuilder gets around this by working like 'List' does. It creates a memory space for a char array slightly larger than the string, and then replaces it if the string grows larger than that. When you call ToString, it trims off the portion of the string you need (so you don't end up with a string longer than you expected filled with whitespace, or worse, something you redacted before calling ToString).

    ...

    So what to do?

    Well, not to long ago a kid came on here asking for a similar thing. But they wanted something of 'static length'. A string that is always X char's long.

    And well... yeah... that's very doable.

    So I did it here for the kid:
    https://forum.unity3d.com/threads/i...eates-garbage-for-the-gc.364818/#post-2361673

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Text;
    4.  
    5. public class StaticString
    6. {
    7.  
    8.     public enum CharAlignment
    9.     {
    10.         Left = 0,
    11.         Right = 1
    12.     }
    13.  
    14.     #region Fields
    15.  
    16.     private static System.Reflection.FieldInfo _sb_str_info = typeof(StringBuilder).GetField("_str",
    17.                                                                                             System.Reflection.BindingFlags.NonPublic |
    18.                                                                                             System.Reflection.BindingFlags.Instance);
    19.     private StringBuilder _sb;
    20.  
    21.     #endregion
    22.  
    23.     #region CONSTRUCTOR
    24.  
    25.     public StaticString(int size)
    26.     {
    27.         _sb = new StringBuilder(new string(' ', size), 0, size, size);
    28.     }
    29.  
    30.     #endregion
    31.  
    32.     #region Properties
    33.  
    34.     public CharAlignment Alignment
    35.     {
    36.         get;
    37.         set;
    38.     }
    39.  
    40.     public string Value
    41.     {
    42.         get { return _sb_str_info.GetValue(_sb) as string; }
    43.     }
    44.  
    45.     #endregion
    46.  
    47.     #region Methods
    48.  
    49.     public void Set(int value)
    50.     {
    51.         const int CHAR_0 = (int)'0';
    52.         _sb.Length = 0;
    53.  
    54.         bool isNeg = value < 0;
    55.         value = System.Math.Abs(value);
    56.         int cap = _sb.Capacity;
    57.         int log = (int)System.Math.Floor(System.Math.Log10(value));
    58.         int charCnt = log + ((isNeg) ? 2 : 1);
    59.         int blankCnt = cap - charCnt;
    60.  
    61.         switch (this.Alignment)
    62.         {
    63.             case CharAlignment.Left:
    64.                 {
    65.                     if (isNeg) _sb.Append('-');
    66.                     int min = System.Math.Max(charCnt - cap, 0);
    67.                     for(int i = log; i >= min; i--)
    68.                     {
    69.                         int pow = (int)System.Math.Pow(10, i);
    70.                         int digit = (value / pow) % 10;
    71.                         _sb.Append((char)(digit + CHAR_0));
    72.                     }
    73.  
    74.                     for (int i = 0; i < blankCnt; i++)
    75.                     {
    76.                         _sb.Append(' ');
    77.                     }
    78.                 }
    79.                 break;
    80.             case CharAlignment.Right:
    81.                 {
    82.                     for (int i = 0; i < blankCnt; i++)
    83.                     {
    84.                         _sb.Append(' ');
    85.                     }
    86.  
    87.                     if (isNeg) _sb.Append('-');
    88.                     int min = System.Math.Max(charCnt - cap, 0);
    89.                     for (int i = log; i >= min; i--)
    90.                     {
    91.                         int pow = (int)System.Math.Pow(10, i);
    92.                         int digit = (value / pow) % 10;
    93.                         _sb.Append((char)(digit + CHAR_0));
    94.                     }
    95.                 }
    96.                 break;
    97.         }
    98.     }
    99.  
    100.     #endregion
    101.  
    102. }
    103.  
    Of course it's specifically for int to string, but the general idea is there. You can use it to write your own version, but get the string out of StringBuilder garbage free.

    Just understanding the limitations of the 'static length' aspect... since changing the size of the string is really what is creating the garbage.
     
    Dave-Carlile likes this.
  7. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
    An issue I've had with this approach is that UI components wouldn't detect changing values - in some Unity release (don't recall which one) it stopped working and the best I could determine is that the text fields in the UI controls would see the same reference so would assume the field hadn't changed.

    Something like:

    Code (csharp):
    1. public string text
    2. {
    3.   get { return _text }
    4.   set {
    5.     if (! _text.ReferenceEquals(value)
    6.     {
    7.        _text = value;
    8.        // do stuff to update the display
    9.     }
    10.   }
    11. }
    That is an assumption on my part though. I never dug into the code, but it was definitely a case where strings stopped updating on the screen after a Unity upgrade. When I went back to a normal ToString() the strings updated, so I left it that way to be addressed later. Now is later.

    It sure sounds like there is no pure C# way to get that final string without the reflection trick. I probably just need to be much more robust about not building strings unless values are changing, and doing the garbage free numeric conversion things.
     
  8. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
    Here's more detail.

    This the text property implementation.

    Code (CSharp):
    1.         public virtual string text
    2.         {
    3.             get
    4.             {
    5.                 return m_Text;
    6.             }
    7.             set
    8.             {
    9.                 if (String.IsNullOrEmpty(value))
    10.                 {
    11.                     if (String.IsNullOrEmpty(m_Text))
    12.                         return;
    13.                     m_Text = "";
    14.                     SetVerticesDirty();
    15.                 }
    16.                 else if (m_Text != value)
    17.                 {
    18.                     m_Text = value;
    19.                     SetVerticesDirty();
    20.                     SetLayoutDirty();
    21.                 }
    22.             }
    23.         }
    The m_Text != value is the key piece. Now the reflection thing. Note that for later .NET versions I had to change the field name to m_StringValue (which is another reason I don't like mucking with hidden internals).


    Code (CSharp):
    1.              System.Reflection.FieldInfo sb_str_info = typeof(StringBuilder).GetField("m_StringValue",
    2.                                                                         System.Reflection.BindingFlags.NonPublic |
    3.                                                                         System.Reflection.BindingFlags.Instance);
    4.              StringBuilder sb = new StringBuilder("abc", 3);
    5.              string v1 = sb_str_info.GetValue(sb) as string;
    6.  
    7.              sb.Length = 0;
    8.              sb.Append("xyz");
    9.              string v2 = sb_str_info.GetValue(sb) as string;
    10.  
    11.              Console.WriteLine(v1 == v2 ? "Equal" : "Not Equal");
    12.  
    This code prints out Equal, because it sees that it's the same string reference. I suspect that in the original UGUI code there was no checking to see if the string changed before dirtying vertices and layout. That got added, and not it doesn't think the string changes because the reference itself didn't change. I haven't tested that behavior with the latest Unity version but I suspect it would still be an issue. Or as always there's the possibility that I'm just being dumb and missing something obvious.

    Edit: And hmmm, that might just be me calling SetVerticesDirty myself - seems like I would have tried that before though, but perhaps not.
     
    Last edited: Oct 24, 2016
  9. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
    That is indeed the solution. After updating the StringBuilder we have to manually dirty the text component so it will update itself with the new string.

    So to reiterate...

    - Must use reflection to get at the hidden string field inside of the StringBuilder instance. This will likely break when the Mono backend is updated - at least it's a different field name in .NET now.
    - Set the text property on the Text component, or whatever property you're changing, to the string field reference - only have to do that one time and it won't change from then on
    - Clear the StringBuilder, replacing the entire value with spaces, concatenate as needed, using the aforementioned methods to append numeric values in a non-garbage-generating way
    - Force the Text component to rebuild itself by calling SetVerticesDirty or SetAllDirty
     
  10. Klaus-Eiperle

    Klaus-Eiperle

    Joined:
    Mar 10, 2012
    Posts:
    41
    The solution is quite simple. For generating the string, I used StaticString and because the Unity UI is not updating, I had the idea to simply use two StringBuilder variables and each frame I use the other. Then the UI updates. :)

    Code (CSharp):
    1. StaticString _staticString1 = new StaticString(8);
    2. StaticString _staticString2 = new StaticString(8);
    3.  
    4. void Update() {
    5.   if (Time.frameCount % 2 == 0) {
    6.     _staticString1.Alignment = StaticString.CharAlignment.Left;
    7.     _staticString1.Set( myIntegerValue) );
    8.     my_cached_text_GUI_object.text = _staticString1.Value;
    9.   } else {
    10.     _staticString2.Alignment = StaticString.CharAlignment.Left;
    11.     _staticString2.Set( myIntegerValue) );
    12.     my_cached_text_GUI_object.text = _staticString2.Value;
    13.   }
    14. }
     
    Last edited: Jan 28, 2019
  11. tthorjen

    tthorjen

    Joined:
    Nov 12, 2013
    Posts:
    2
    A simple solution is to just preallocate all the strings you need, e.g. allocate a string[100] array and just lookup the required string at runtime.
     
  12. doctorpangloss

    doctorpangloss

    Joined:
    Feb 20, 2013
    Posts:
    270
    If you're using Unity UI, surely you realize that there are allocations when you render the text!