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

Hey guys,

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

private int testerInt = 4;
private string testerChar = "";

void Update(){
        testerChar = testerInt.ToString ();
}

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!

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.

1 Like

I bet it was an entry similar to the one by Defective Studios that shows using StringBuilder. I haven’t really examined or tried out the sample code, but the comments are stating something was changed that prevents the code from working in Unity 5.

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:

using UnityEngine;
using System.Collections.Generic;

public class BlarghScript : MonoBehaviour {

    void Start()
    {
        var sb = new System.Text.StringBuilder();
        sb.Append("Hello World");
        var str = GarbageFreeString(sb);
        sb.Length = 0;
        sb.Append("Good-bye");

        Debug.Log(str);
    }


    public static string GarbageFreeString(System.Text.StringBuilder sb)
    {
        string str = (string)typeof(System.Text.StringBuilder).GetField(
            "_str",
            System.Reflection.BindingFlags.NonPublic |
            System.Reflection.BindingFlags.Instance).GetValue(sb);
      
        return str;
    }

}

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.

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.

Yes this was the post I was talking about.

Well that’s not good :frowning: 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.

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.

I was bored, so I wrote a really basic representation of what it’d be like.

using UnityEngine;
using System.Text;

public class StaticString
{

    public enum CharAlignment
    {
        Left = 0,
        Right = 1
    }

    #region Fields

    private static System.Reflection.FieldInfo _sb_str_info = typeof(StringBuilder).GetField("_str",
                                                                                            System.Reflection.BindingFlags.NonPublic |
                                                                                            System.Reflection.BindingFlags.Instance);
    private StringBuilder _sb;

    #endregion

    #region CONSTRUCTOR

    public StaticString(int size)
    {
        _sb = new StringBuilder(new string(' ', size), 0, size, size);
    }

    #endregion

    #region Properties

    public CharAlignment Alignment
    {
        get;
        set;
    }

    public string Value
    {
        get { return _sb_str_info.GetValue(_sb) as string; }
    }

    #endregion

    #region Methods

    public void Set(int value)
    {
        const int CHAR_0 = (int)'0';
        _sb.Length = 0;

        bool isNeg = value < 0;
        value = System.Math.Abs(value);
        int cap = _sb.Capacity;
        int log = (int)System.Math.Floor(System.Math.Log10(value));
        int charCnt = log + ((isNeg) ? 2 : 1);
        int blankCnt = cap - charCnt;

        switch (this.Alignment)
        {
            case CharAlignment.Left:
                {
                    if (isNeg) _sb.Append('-');
                    int min = System.Math.Max(charCnt - cap, 0);
                    for(int i = log; i >= min; i--)
                    {
                        int pow = (int)System.Math.Pow(10, i);
                        int digit = (value / pow) % 10;
                        _sb.Append((char)(digit + CHAR_0));
                    }

                    for (int i = 0; i < blankCnt; i++)
                    {
                        _sb.Append(' ');
                    }
                }
                break;
            case CharAlignment.Right:
                {
                    for (int i = 0; i < blankCnt; i++)
                    {
                        _sb.Append(' ');
                    }

                    if (isNeg) _sb.Append('-');
                    int min = System.Math.Max(charCnt - cap, 0);
                    for (int i = log; i >= min; i--)
                    {
                        int pow = (int)System.Math.Pow(10, i);
                        int digit = (value / pow) % 10;
                        _sb.Append((char)(digit + CHAR_0));
                    }
                }
                break;
        }
    }

    #endregion

}

It’s a static length string. It’s mutable. It comes with the caveats I outlined already.

But it’s got no garbage.

4 Likes

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:

private String[] intToString = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"};

int slot = 6;
tempStatusEffect..GetComponent<Text> ().text = intToString[slot];
//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.

public StaticString staticString = new StaticString(20);
staticString.Set (33);
Debug.Log ("VALUE: " + staticString.Value);

EDIT: I figured it out, I was being silly.

How can I use the CharAlignment?


found it out:
myStaticStringName.Alignment = StaticString.CharAlignment.Left;

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?

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:

var fields = typeof(StringBuilder).GetFields(BindingFlags.NonPublic |
        BindingFlags.Instance);
        foreach (var field in fields)
        {
            Debug.Log($"field name {field.Name} type : {field.FieldType}");
        }

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.

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.

2 Likes

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 :slight_smile: TextMesh Pro Documentation:.

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.

public class StaticString
    {

        public enum CharAlignment
        {
            Left = 0,
            Right = 1
        }

        #region CONSTRUCTOR

        public StaticString(int size, char fillCharacter = '\0', CharAlignment alignment = CharAlignment.Left)
        {
            _buffer = new char[size];
            _capacity = size;
            Alignment = alignment;
            FillCharacter = fillCharacter;
        }

        #endregion

        #region Methods

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Set(int value)
        {
            const int CHAR_0 = '0';
            var isNeg = value < 0;
           // value = Math.Abs(value);
            var cap = _capacity;
            var log = (int)Math.Floor(Math.Log10(value));
            var charCnt = log + (isNeg ? 2 : 1);
            var blankCnt = cap - charCnt;

            var currentCharIndex = 0;
            switch (Alignment)
            {
                case CharAlignment.Left:
                {
                    if (isNeg)
                    {
                        _buffer[0] = '-';
                        currentCharIndex++;
                    }
                    var min = Math.Max(charCnt - cap, 0);
                    for(var i = log; i >= min; i--)
                    {
                        var pow = (int)Math.Pow(10, i);
                        var digit = value / pow % 10;
                        _buffer[currentCharIndex++] = (char)(digit + CHAR_0);
                    }

                    for(var i = 0; i < blankCnt; i++)
                    {
                        _buffer[currentCharIndex++] = FillCharacter;
                    }
                }
                    break;
                case CharAlignment.Right:
                {
                    for(var i = 0; i < blankCnt; i++)
                    {
                        _buffer[currentCharIndex++] = FillCharacter;
                    }

                    if (isNeg)
                    {
                        _buffer[currentCharIndex] = '-';
                        currentCharIndex++;
                    }
                    var min = Math.Max(charCnt - cap, 0);
                    for(var i = log; i >= min; i--)
                    {
                        var pow = (int)Math.Pow(10, i);
                        var digit = value / pow % 10;
                        _buffer[currentCharIndex++] = (char)(digit + CHAR_0);
                    }
                }
                    break;
            }
        }

        #endregion

        #region Fields

        private readonly char[] _buffer;
        private int _capacity;

        #endregion

        #region Properties

        // ReSharper disable once MemberCanBePrivate.Global
        public CharAlignment Alignment
        {
            get;
            set;
        }

        public string Value => new string(_buffer);
        public char[] Buffer => _buffer;
        // ReSharper disable once MemberCanBePrivate.Global
        /// <summary>
        ///     The character to fill the remaining space with. For example, if you create a StaticString(6, "-")
        ///     It will contain 6 characters. If you set it too 123 with Alignment Left it will return "123---"
        /// </summary>
        public char FillCharacter { get; set; }

        #endregion
    }

Then you can just use it like this:

_scoreString.Set(score);
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.

1 Like

SetCharArray still make GC

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.

1 Like