Search Unity

Int to String conversion - toString() creates garbage for the GC

Discussion in 'Scripting' started by Azmar, Oct 30, 2015.

  1. Azmar

    Azmar

    Joined:
    Feb 23, 2015
    Posts:
    246
    Hey guys,

    I made a simple clean scene to test int to string conversion:

    Code (CSharp):
    1. private int testerInt = 4;
    2. private string testerChar = "";
    3.  
    4. void Update(){
    5.         testerChar = testerInt.ToString ();
    6. }
    7.  
    This creates 28B of garbage every frame, how do I do proper conversion from int to string without creating any garbage? I have already scoured the internet, and there was a post that says a way to fix it, but it broke again in Unity 5. So I am using Unity 5, and cannot come up with a solution.

    Thanks for the help!
     
  2. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Strings are immutable. That means every time you create a new string, you must abandon the old one to the GC.

    There are ways around it. The best is simply not to use and abandon strings in Update. If you were only to use ToString() when the int was changed your garbage would drop dramatically.

    If you really want to avoid the string allocation you could create and reuse an array of char.

    Also linking the post that supposedly fixed this would help. Most breaking changes between 4.x and 5.x were fairly simple to fix. Unless it relies on wheel physics.
     
    Naman_Jain likes this.
  3. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,183
    Last edited: Oct 30, 2015
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Nah, that should still work in unity5. The mscorlib dll that StringBuilder comes from still implements its private fields the same way.

    There is one downside, the string you're extracting is supposed to be immutable. But StringBuilder is treating it as mutable.

    How it does this is it reuses the block of memory that represents the string. When you call 'ToString' it actually pulls out the chunk of the string you really need. I'll give an example:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4.  
    5. public class BlarghScript : MonoBehaviour {
    6.  
    7.     void Start()
    8.     {
    9.         var sb = new System.Text.StringBuilder();
    10.         sb.Append("Hello World");
    11.         var str = GarbageFreeString(sb);
    12.         sb.Length = 0;
    13.         sb.Append("Good-bye");
    14.  
    15.         Debug.Log(str);
    16.     }
    17.  
    18.  
    19.     public static string GarbageFreeString(System.Text.StringBuilder sb)
    20.     {
    21.         string str = (string)typeof(System.Text.StringBuilder).GetField(
    22.             "_str",
    23.             System.Reflection.BindingFlags.NonPublic |
    24.             System.Reflection.BindingFlags.Instance).GetValue(sb);
    25.      
    26.         return str;
    27.     }
    28.  
    29. }
    30.  
    We call 'GarbageFreeString' before modifying the StringBuilder. So you'd think it'd contain 'Hello World', but it doesn't.

    But its even weirder. We reset the length to 0, and then stuck 'Good-bye' in there. This is 3 characters shorter than "Hello World". What actually gets written is...

    "Good-byerld"

    Yeah, the last 3 characters are still there. Because the block of memory that represented "Hello World" was just reused. StringBuilder just moved its head position to 0 on Length = 0, then started filling from there with Good-bye. Which didn't reach the end. So it still contains the chars for rld in it.

    If you had called 'GetString', it would have extracted the substring from '_str' that should have been used.
     
    Last edited: Oct 30, 2015
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Oh, and StringBuilder still requires the int to be converted to a string. So you'd still get garbage.

    Really there's no way to avoid the garbage cost when creating a string from a none string type.
     
  6. Azmar

    Azmar

    Joined:
    Feb 23, 2015
    Posts:
    246
    Yes this was the post I was talking about.
     
  7. Azmar

    Azmar

    Joined:
    Feb 23, 2015
    Posts:
    246
    Well that's not good :( How about I know the size of the string will always be 3 characters or less. I could premake the length and cache it? So when I convert my "3" it only takes up 1 character.
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Technically... yessish.

    It's going to be weird to pull such a thing off.

    And it will have weird implications. Because you're basically creating a mutable string. So what I described with StringBuilder is an issue. You can only use this string for its thing, and only its thing, and wherever it's used it shouldn't care what the state of it is... and that it can change on a whim.
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    I was bored, so I wrote a really basic representation of what it'd be like.

    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.  
    It's a static length string. It's mutable. It comes with the caveats I outlined already.

    But it's got no garbage.
     
  10. Azmar

    Azmar

    Joined:
    Feb 23, 2015
    Posts:
    246
    Not gonna lie, I have no clue how to use this beauty. Can I please get a use case out of this? Thanks!

    And I thought my solution for first 20 ints were a good idea LOL, which in my case the value will never go above 20:
    Code (CSharp):
    1. private String[] intToString = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"};
    2.  
    3. int slot = 6;
    4. tempStatusEffect..GetComponent<Text> ().text = intToString[slot];
    5. //Give it 6!!
    But yeah I for sure need a better solution that I can use everywhere in my code as I have high values like 10,000 to be converted. Obviously my way will not work.


    Code (CSharp):
    1. public StaticString staticString = new StaticString(20);
    2. staticString.Set (33);
    3. Debug.Log ("VALUE: " + staticString.Value);
    EDIT: I figured it out, I was being silly.
     
    Last edited: Oct 30, 2015
  11. Klaus-Eiperle

    Klaus-Eiperle

    Joined:
    Mar 10, 2012
    Posts:
    41
    How can I use the CharAlignment?

    ...found it out:
    myStaticStringName.Alignment = StaticString.CharAlignment.Left;
     
    Last edited: Jan 28, 2019
  12. Klaus-Eiperle

    Klaus-Eiperle

    Joined:
    Mar 10, 2012
    Posts:
    41
    I have switched from Unity 5.6.6 to Unity 2019 and see an error.

    StaticString _mySB = new StaticString ( 2 );
    _mySB.Set ( 33 );
    Debug.Log ( _mySB.Value );

    Then I get an error in the line Debug.Log: NullReferenceException: Object reference not set to an instance of an object

    What should I change?
     
    Last edited: Nov 6, 2019
  13. BinaryByron

    BinaryByron

    Joined:
    Sep 16, 2016
    Posts:
    12
    A bit late to the party haha. It's throwing that exception because it can't find the
    _str
    Field using reflection anymore. If you iterate through the StringBuilder fields using reflection you can find what the new field is called like this:
    Code (CSharp):
    1. var fields = typeof(StringBuilder).GetFields(BindingFlags.NonPublic |
    2.         BindingFlags.Instance);
    3.         foreach (var field in fields)
    4.         {
    5.             Debug.Log($"field name {field.Name} type : {field.FieldType}");
    6.         }
    Results in:

    field name m_ChunkChars type : System.Char[]


    you'll see that the field is now called
    m_ChunkChars
    . In the
    StaticString
    just changed
    _str
    ->
    m_ChunkCars
    to get it working again.
     
  14. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Yeah, the problem with reflecting things out of encapsulated data types (classes) is that said encapsulation is supposed to allow the developers to modify the internals without support consequence because the way you're supposed to access it is through the public interface.

    The code above using '_str' was basically a hack.

    And as with many hacks, its support will be specific to the version you're hacking. Future versions may change and the hack is no longer viable. Unity has since moved on to newer versions of .net, and therefore the underlying library that StringBuilder is from has changed.
     
    BinaryByron and mopthrow like this.
  15. BinaryByron

    BinaryByron

    Joined:
    Sep 16, 2016
    Posts:
    12
    Yep. To add to that, this code no longer works. Value is always returned as
    null
    as the cast from StringBuilder to string always falls/ returns null. I'd recommend to anyone wanting to do something this is, it's better to just used a fixed length char[] and TextMeshProGUI. TextMeshProGUI can take a char[] using
    SetCharArray so you don't have to set it with a string using
    .text = myScore.ToString()
    for example. This gives the added benefit of saving additional string allocations inside TextMeshProGUI. Even states it in the old documentation :) http://digitalnativestudios.com/textmeshpro/docs/ScriptReference/TextMeshPro-SetCharArray.html.

    If anyone is interested, here's the same class just adjusted a fit to use char array, I haven't cleaned it up, just a quick hack.

    Code (CSharp):
    1. public class StaticString
    2.     {
    3.  
    4.         public enum CharAlignment
    5.         {
    6.             Left = 0,
    7.             Right = 1
    8.         }
    9.  
    10.         #region CONSTRUCTOR
    11.  
    12.         public StaticString(int size, char fillCharacter = '\0', CharAlignment alignment = CharAlignment.Left)
    13.         {
    14.             _buffer = new char[size];
    15.             _capacity = size;
    16.             Alignment = alignment;
    17.             FillCharacter = fillCharacter;
    18.         }
    19.  
    20.         #endregion
    21.  
    22.         #region Methods
    23.  
    24.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    25.         public void Set(int value)
    26.         {
    27.             const int CHAR_0 = '0';
    28.             var isNeg = value < 0;
    29.            // value = Math.Abs(value);
    30.             var cap = _capacity;
    31.             var log = (int)Math.Floor(Math.Log10(value));
    32.             var charCnt = log + (isNeg ? 2 : 1);
    33.             var blankCnt = cap - charCnt;
    34.  
    35.             var currentCharIndex = 0;
    36.             switch (Alignment)
    37.             {
    38.                 case CharAlignment.Left:
    39.                 {
    40.                     if (isNeg)
    41.                     {
    42.                         _buffer[0] = '-';
    43.                         currentCharIndex++;
    44.                     }
    45.                     var min = Math.Max(charCnt - cap, 0);
    46.                     for(var i = log; i >= min; i--)
    47.                     {
    48.                         var pow = (int)Math.Pow(10, i);
    49.                         var digit = value / pow % 10;
    50.                         _buffer[currentCharIndex++] = (char)(digit + CHAR_0);
    51.                     }
    52.  
    53.                     for(var i = 0; i < blankCnt; i++)
    54.                     {
    55.                         _buffer[currentCharIndex++] = FillCharacter;
    56.                     }
    57.                 }
    58.                     break;
    59.                 case CharAlignment.Right:
    60.                 {
    61.                     for(var i = 0; i < blankCnt; i++)
    62.                     {
    63.                         _buffer[currentCharIndex++] = FillCharacter;
    64.                     }
    65.  
    66.                     if (isNeg)
    67.                     {
    68.                         _buffer[currentCharIndex] = '-';
    69.                         currentCharIndex++;
    70.                     }
    71.                     var min = Math.Max(charCnt - cap, 0);
    72.                     for(var i = log; i >= min; i--)
    73.                     {
    74.                         var pow = (int)Math.Pow(10, i);
    75.                         var digit = value / pow % 10;
    76.                         _buffer[currentCharIndex++] = (char)(digit + CHAR_0);
    77.                     }
    78.                 }
    79.                     break;
    80.             }
    81.         }
    82.  
    83.         #endregion
    84.  
    85.         #region Fields
    86.  
    87.         private readonly char[] _buffer;
    88.         private int _capacity;
    89.  
    90.         #endregion
    91.  
    92.         #region Properties
    93.  
    94.         // ReSharper disable once MemberCanBePrivate.Global
    95.         public CharAlignment Alignment
    96.         {
    97.             get;
    98.             set;
    99.         }
    100.  
    101.         public string Value => new string(_buffer);
    102.         public char[] Buffer => _buffer;
    103.         // ReSharper disable once MemberCanBePrivate.Global
    104.         /// <summary>
    105.         ///     The character to fill the remaining space with. For example, if you create a StaticString(6, "-")
    106.         ///     It will contain 6 characters. If you set it too 123 with Alignment Left it will return "123---"
    107.         /// </summary>
    108.         public char FillCharacter { get; set; }
    109.  
    110.         #endregion
    111.     }
    Then you can just use it like this:

    Code (CSharp):
    1. _scoreString.Set(score);
    2. displayText.SetCharArray(_scoreString.Buffer);
    Another performance tip for stuff like showing score etc in the UI. You don't really have to update it every frame. Maybe just update it 10 or 5 or whatever times a second.
     
    DungDajHjep likes this.
  16. DungDajHjep

    DungDajHjep

    Joined:
    Mar 25, 2015
    Posts:
    202
    SetCharArray still make GC
     
  17. INeatFreak

    INeatFreak

    Joined:
    Sep 12, 2018
    Posts:
    46
    Only in editor because it needs to update the editor text field in inspector which requires a new String() creation.

    I've verified it in build on Unity 2020.3.10f and TextMeshPro 3.0.6 versions.
     
    DungDajHjep likes this.